Introduction
Alpha Note: Cloesce is under active development, expanding its feature set as it pushes towards full Cloudflare support, across any language. In this alpha, breaking changes can occur between releases.
The Cloesce Compiler converts object definitions into a full stack Cloudflare application.
Inspired by
- Entity Framework
- NestJS
- ASP.NET
- Swagger Codegen
- gRPC
- and Infrastructure as Code (IaC)
Cloesce is not just a ORM, Migrations Engine, Web Framework, Runtime Validation Library, IaC tool, or API Generator. It is all of these things and more, wrapped in a clean paradigm that makes building Cloudflare applications a breeze.
@Model(["GET", "SAVE", "LIST"])
class User {
id: Integer;
name: String;
posts: Post[];
@KV("user/settings/{id}", namespace)
settings: KValue<unknown>;
@R2("user/avatars/{id}.png", bucket)
avatar: R2Object;
@POST
async hello(): User {
// D1, KV and R2 all hydrated here!
return this;
}
}
# Coming in a later release!
// Coming in a later release!
How simple can full stack development get?
Contributing
Contributions are welcome at all levels. Join our Discord to discuss ideas, report issues, or get help getting started. Create an issue on GitHub if you find a bug or have a feature request.
Coalesce
Check out Coalesce, an accelerated web app framework for Vue.js and Entity Framework by IntelliTect. Cloesce takes much of its inspiration from Coalesce (Cloesce = Cloudflare + Coalesce).
TypeDoc
An API reference generated using TypeDoc for the Cloesce TypeScript library can be found here.
Getting Started
Welcome to the Getting Started guide! This document will help you set up a basic Cloesce project using the create-cloesce template. We will discuss:
- Installing Cloesce
- A basic project structure
- Building, running and deploying your Cloesce application
Cloesce runs entirely on Cloudflare Workers, so familiarity with Workers and Wrangler is recommended. If you are new to Workers, check out the Cloudflare Workers documentation.
Installation
Alpha Note: Cloesce supports only TypeScript to TypeScript compilation as of Alpha v0.1.0. Support for additional languages will be added in future releases.
The simplest way to get a Cloesce project up and running is to use the create-cloesce template.
This template sets up a basic Cloesce project structure with all the necessary dependencies and configurations, example Models, and example tests to help get you started quickly. The template includes a sample HTML frontend with Vite which should be replaced with your frontend of choice.
Prerequisites
- Sign up for a Cloudflare account
- Install Node.js (version
16.17.0or later)
create-cloesce
To create a new Cloesce project using the create-cloesce template, run the following command in your terminal:
$ npx create-cloesce my-cloesce-app
After running this command, navigate into your new project directory:
$ cd my-cloesce-app
A simple project structure is set up for you:
├── src/
│ ├── data/ # Example Cloesce Models
│ └── web/ # Frontend web assets
├── tests/ # Unit tests for example Models
├── migrations/ # Database migration files
├── cloesce.config.json # Cloesce compiler configuration
└── package.json # Project dependencies and scripts
Exploring the Template
After creating your project with create-cloesce, several example files are included to help you get started. Below is an overview of those files and their purpose.
Wrangler Environment
All Cloudflare Workers define a a set of bindings that provision resources such as D1 databases, R2 buckets, KV namespaces, and miscellaneous environment variables.
Cloesce uses a class decorated with @WranglerEnv to define the Wrangler Environment for your application, tailoring to the resources you need.
In src/data/main.ts, a basic Wrangler Environment has been defined.
@WranglerEnv
export class Env {
db: D1Database;
bucket: R2Bucket;
myVariable: string;
}
The above implementation of Env defines a Wrangler environment with a D1 database binding named db, an R2 bucket named bucket, and a string environment variable named myVariable.
A typical Cloudflare Worker defines these bindings in a wrangler.toml file, but Cloesce generates this file for you during compilation based on the @WranglerEnv class.
Read more about the Wrangler Environment in the Wrangler Environment chapter.
Custom Main Function
Cloudflare Workers are serverless functions that run at Cloudflare’s edge and respond to HTTP requests. Each Worker defines an entry point function through which all requests are routed.
Cloesce allows this same functionality through a custom main definition (seen in src/data/main.ts)
export default async function main(
request: Request,
env: Env,
app: CloesceApp,
ctx: ExecutionContext
): Promise<Response> {...}
Just like the standard Workers entrypoint, this function receives the inbound Request, the Wrangler Environment defined by the decorated @WranglerEnv class, and an ExecutionContext for managing background tasks.
Additionally, it receives a CloesceApp instance that you can use to handle routing and Model operations.
Read more about custom main functions in the Middleware chapter.
Example Models
Models are the core building blocks of a Cloesce application. They define exactly how your data is structured, what relationships exist between different data entities, and what API endpoints will be generated to interact with that data.
Unlike other ORMs, Cloesce Models are not limited to just relational data stored in a SQL database. Models can also include data stored in R2 buckets, KV namespaces, or inject external services.
In src/data/Models.ts you will find two example Models, Weather and WeatherReport.
Weather Code Snippet
@Model()
export class Weather {
id: Integer;
weatherReportId: Integer;
weatherReport: WeatherReport | undefined;
dateTime: Date;
location: string;
temperature: number;
condition: string;
@R2("weather/photo/{id}", "bucket")
photo: R2ObjectBody | undefined;
static readonly withPhoto: IncludeTree<Weather> = {
photo: {}
}
@POST
async uploadPhoto(@Inject env: Env, stream: ReadableStream) {... }
@GET
downloadPhoto() {... }
}
WeatherReport Code Snippet
@Model(["GET", "LIST", "SAVE"])
export class WeatherReport {
id: Integer;
title: string;
description: string;
weatherEntries: Weather[];
static readonly withWeatherEntries: IncludeTree<WeatherReport> = {
weatherEntries: {}
}
}
The Weather Model conists of:
| Feature | Type / Description | Source / Layer |
|---|---|---|
id | Primary Key | D1 |
weatherReport | One-to-One relationship | D1 |
dateTime | Scalar column | D1 |
temperature | Scalar column | D1 |
condition | Scalar column | D1 |
photo | R2 object, key format weather/photo/{id} | R2 |
withPhoto | IncludeTree | Cloesce |
uploadPhoto | API endpoint | Workers |
downloadPhoto | API endpoint | Workers |
The WeatherReport Model consists of:
| Feature | Type / Description | Source / Layer |
|---|---|---|
id | Primary Key | D1 |
title | Scalar column | D1 |
summary | Scalar column | D1 |
weatherEntries | One-to-Many relationship with Weather | D1 |
withWeatherEntries | IncludeTree | Cloesce |
GET | Generated CRUD operation | Workers |
SAVE | Generated CRUD operation | Workers |
LIST | Generated CRUD operation | Workers |
Read more about how models work in the Models chapter.
Building and Migrating
Building a Cloesce project generally consists of three steps:
- Compilation
- Running database migrations
- Building your frontend code.
Compiling
In your project directory, run the following command to compile your Cloesce Models:
$ npx cloesce compile
This command looks for a cloesce.config.json file in your project root, which contains configuration settings for the Cloesce compiler. If the file is not found, or settings are omitted, default values will be used.
After compilation, a .generated folder is created in your project root. This should not be committed to source control, as it is regenerated on each build. The folder contains:
-
cidl.jsonThe Cloesce Interface Definition Language file, representing your data Models and their relationships. This file is used internally by Cloesce for generating client code, migrations, and running the Cloudflare Worker.
-
client.ts:The generated client code for accessing your Models from the frontend. Import this file in your frontend code to interact with your Cloesce Models over HTTP.
-
workers.ts:The generated Cloudflare Worker code with all linked dependencies (including your custom
mainfunction if defined). This file is the entry point for your Cloudflare Worker and is referenced in the generatedwrangler.toml.
Alpha Note:
wrangler.jsoncis not fully supported. Please usewrangler.tomlfor now.
Generating Migrations
To generate database migration files based on changes to your Cloesce Models, run the following command:
$ npx cloesce migrate <migration-name>
This command compares your current Cloesce Models against the last applied migration and generates a new migration file in the migrations/ folder with the specified <migration-name>. The migration file contains SQL statements to update your D1 database schema to match your Models.
You must apply the generated migrations to your D1 database using the Wrangler CLI:
$ npx wrangler d1 migrations apply <database-binding-name>
Running
After compiling and applying migrations, you can build and run your Cloudflare Worker locally using Wrangler:
$ npx wrangler dev --port <port-number>
Deploying
Alpha Note: Deployment is not yet enhanced by Cloesce and has room for improvement.
With your application built and your database migrated, you’re ready to deploy your Cloesce application to Cloudflare Workers. Deployment is done through the Wrangler CLI.
-
Modify
cloesce.config.jsonEnsure your
cloesce.config.jsonfile is correctly configured for production, including the production Workers URL.NOTE: Workers URLs must have some path component (e.g.,
https://my-app.workers.dev/apihas/api). -
Configure Wrangler bindings
Open your
wrangler.tomland set all required binding IDs (e.g.,kv_namespaces,d1_databases,r2_buckets) to their production values.Example:
[[kv_namespaces]] binding = "kv" id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -
Build your application
Run the compile command to generate the necessary files for deployment:
$ npx cloesce compile -
Deploy using Wrangler
Publish your application to Cloudflare Workers:
$ npx wrangler deploy -
Deploy your frontend
If you have a frontend application (e.g., built with Vite), build and deploy it to your preferred hosting service. For example, with Cloudflare Pages
$ npx wrangler pages deploy ./dist
Models
Models are used to define the:
- Source of data (D1, KV, R2)
- Structure of data (properties and relationships)
- REST operations that can be performed on the data (CRUD methods, custom methods)
- Generated Client API
A single Cloesce Model encapsulates an entity that exists across the stack. Suprisingly, they’re only a couple of lines of code!
In this chapter, we will explore the various features of Cloesce Models, including defining D1, KV and R2 backed properties, relationships between Models, generated CRUD methods, and custom methods.
Basic D1 Backed Model
In this section we will explore the basic properties of a D1 backed Model in Cloesce.
Cloudflare D1 is a serverless SQL database built on SQLite for Workers.
Defining a Model
Compilation in Cloesce is composed of three phases: Extraction, Analysis and Code Generation.
During Extraction, the compiler scans your source files (designated with *.cloesce.ts) for Model definitions. Models are defined using the @Model() decorator.
import { Model, Integer, PrimaryKey } from "cloesce/backend";
@Model()
export class User {
@PrimaryKey
id: Integer;
name: string;
}
The above code defines a Model “User” with several properties:
| Property | Description |
|---|---|
User | Cloesce infers from the class attributes that this model is backed by a D1 table User |
id | Integer property decorated with @PrimaryKey, indicating it is the model’s primary key. |
name | String property representing the user’s name; stored as a regular column in the D1 database. |
Models do not have constructors as they should not be manually instantiated. Instead, use the ORM functions to create, retrieve and update Model instances. For tests, consider using
Object.assign()to create instances of Models with specific property values.
TIP: Using the
@PrimaryKeydecorator is optional if your primary key property is namedidor<className>Id(in any casing, ie snake case, camel case, etc). The compiler will automatically treat a property namedidas the primary key.
Supported D1 Column Types
Cloesce supports a variety of column types for D1 Models. These are the supported TypeScript types and their corresponding SQLite types:
| TypeScript Type | SQLite Type | Notes |
|---|---|---|
Integer | INTEGER | Represents an integer value |
string | TEXT | Represents a string value |
boolean | INTEGER | 0 for false, 1 for true |
Date | TEXT | Stored in ISO 8601 format |
number | REAL | Represents a floating-point number |
Uint8Array | BLOB | Represents binary data |
All of these types by themselves are NOT NULL by default. To make a property nullable, you can use a union with null e.g., property: string | null;. undefined is reserved for Navigation Properties and cannot be used to indicate nullability.
Notably, an Integer primary key is automatically set to AUTOINCREMENT in D1, so you don’t need to manually assign values to it when creating new records (useful for the ORM functions).
Migrating the Database
The standard Cloesce compilation command does not perform database migrations. To create or update the D1 database schema based on your Model definitions, you need to run the migration command:
$ npx cloesce compile # load the latest Model definitions
$ npx cloesce migrate <migration name>
TIP: Any change in a D1 backed Model definition (adding, removing or modifying properties, renaming Models, etc) requires a new migration to be created. The migration command will generate a new migration file in the
migrations/directory.
Navigation Properties
In the previous section, we built a basic D1 model with scalar properties. However, relational databases like Cloudflare D1 often involve more complex relationships between tables.
In this section, we will explore Navigation Properties which allow us to define relationships between different Models.
Foreign Keys
Before diving into Navigation Properties, it’s essential to understand their source: foreign keys. Foreign keys are scalar properties that reference the Primary Key of another Model, establishing a relationship between two Models.
Foreign keys directly translate to SQLite FOREIGN KEY constraints in the underlying D1 database.
For example, let’s say we want to create a relationship between Person and Dog, where a Person can have one Dog
import { Model, Integer, ForeignKey } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
}
@Model()
export class Person {
id: Integer;
@ForeignKey(Dog)
dogId: Integer;
}
The Person Model has a foreign key property dogId, which references the primary key of the Dog Model. This establishes a relationship where each person can be associated with one dog.
NOTE: Cloesce does not allow circular foreign key relationships (and neither does SQLite!).
If you need to model such a relationship, consider marking a foreign key as nullable and managing the relationship at the application level.
Navigation Properties
Inspired by Entity Framework relationship navigations, Cloesce allows you to effortlessly define One to One, One to Many and Many to Many relationships between your Models using Navigation Properties. All navigation properties are D1 backed Models themselves, or arrays of D1 backed Models.
Let’s revisit our Person and Dog Models and add navigation properties to them:
import { Model, Integer, ForeignKey, OneToOne } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
}
@Model()
export class Person {
id: Integer;
@ForeignKey(Dog)
dogId: Integer;
@OneToOne<Dog>(p => p.dogId)
dog: Dog | undefined;
}
In this example, we added a navigation property dog to the Person Model using the @OneToOne decorator.
This property allows us to access the associated Dog instance directly from a Person instance. The type of the navigation property is Dog | undefined, indicating that it may or may not be populated (elaborated on in the Include Trees section).
Just like in Entity Framework, omitting decorators is possible when specific naming conventions are followed. The above code can be reduced to:
import { Model, Integer } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
}
@Model()
export class Person {
id: Integer;
dogId: Integer;
dog: Dog | undefined;
}
Cloesce will automatically infer the relationship based on the property names in a similiar fashion to primary key inference. (dog matches dogId or dog_id in any casing).
One to Many
Let’s modify our Models to allow a Person to have multiple Dogs:
import { Model, Integer, ForeignKey, OneToMany } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
@ForeignKey(Person)
ownerId: Integer;
@OneToMany<Person>(d => d.ownerId)
owner: Person | undefined;
}
@Model()
export class Person {
id: Integer;
@OneToMany<Dog>(d => d.ownerId)
dogs: Dog[];
}
In this example, we added a foreign key ownerId to the Dog Model, referencing the Person Model. The Person Model now has a navigation property dogs, which is an array of Dog instances, representing all dogs owned by that person.
We can omit decorators for OneToMany only if a single ForeignKey exists pointing from Dog to Person. Thus, the above code can be simplified to:
import { Model, Integer } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
ownerId: Integer;
owner: Person | undefined;
}
@Model()
export class Person {
id: Integer;
dogs: Dog[];
}
Many To Many
Many to Many relationships have an intermediate junction table that holds foreign keys to both related Models.
import { Model, Integer } from "cloesce/backend";
@Model()
export class Student {
id: Integer;
courses: Course[];
}
@Model()
export class Course {
id: Integer;
students: Student[];
}
An underlying junction table will be automatically created by Cloesce during migration:
CREATE TABLE IF NOT EXISTS "CourseStudent" (
"left" integer NOT NULL,
"right" integer NOT NULL,
PRIMARY KEY ("left", "right"),
FOREIGN KEY ("left") REFERENCES "Course" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY ("right") REFERENCES "Student" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
Note: The left column lists the model name that comes first alphabetically; the right column lists the one that comes after.
Include Trees
If you try to fetch a Model instance that has Navigation Properties or KV and R2 attributes, you will notice that they are not populated by default (they will be either empty arrays or undefined). This is intentional and is handled by Include Trees.
What are Include Trees?
Include Trees are Cloesce’s response to the overfetching and recursive relationship challenges when modeling a relational database with object-oriented paradigms.
For example, in the Model definition below, how should Cloesce know how deep to go when fetching a Person and their associated Dog?
import { Model, Integer } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
ownerId: Integer;
owner: Person | undefined;
}
@Model()
export class Person {
id: Integer;
dogs: Dog[];
}
// => { id: 1, dogs: [ { id: 1, owner: { id: 1, dogs: [ ... ] } } ] } ad infinitum
If we were to follow this structure naively, fetching a Person would lead to fetching their Dog, which would lead to fetching the same Person again, and so on, infinitely.
Include Trees allow developers to explicitly state which related Navigation Properties should be included in the query results, preventing overfetching. If a Navigation Property is not included in the Include Tree, it will remain undefined (for singular relationships) or an empty array (for collections).
A common convention to follow when writing singular Navigation Properties is to define them as Type | undefined, indicating that they may not be populated unless explicitly included.
All scalar properties (e.g.,
string,number,boolean, etc.) are always included in query results. Include Trees are only necessary for Navigation Properties.
Alpha Note: No default include behavior is implemented yet. All Navigation Properties must be explicitly included using Include Trees.
Creating an Include Tree
We can define Include Trees to specify that when fetching a Person, we want to include their dogs, but not the owner property of each Dog:
import { Model, Integer, IncludeTree } from "cloesce/backend";
@Model()
export class Dog {
id: Integer;
ownerId: Integer;
owner: Person | undefined;
}
@Model()
export class Person {
id: Integer;
dogs: Dog[];
static readonly withDogs: IncludeTree<Person> = {
dogs: {
owner: {
// Left empty to signal no more includes
// ...but we could keep going!
// dogs: { ... }
}
}
};
}
In this example, we defined a static property withDogs on the Person Model that represents an Include Tree. This tree specifies that when fetching a Person, we want to include their dogs, but we do not want to include the owner property of each Dog.
During Cloesce’s extraction phase, the compiler recognizes the IncludeTree type and processes the structure accordingly.
Client code generation will then have the option to use this Include Tree when querying for Person instances. See the Cloesce ORM chapter and Model Methods for more details on how to use Include Trees in queries.
Include Trees are not limited to only D1 backed Models; they can be used with KV and R2 as well.
KV and R2 Properties
D1 is a powerful relational database solution, but sometimes developers need to work with other types of storage for specific use cases. Cloesce supports integrating Cloudflare KV and Cloudflare R2 storage directly into your Models, allowing you to leverage these storage solutions alongside D1 databases.
Defining a Model with KV
Cloudflare KV is a globally distributed key-value storage system. Along with a key and value, KV entries can also have associated metadata.
Cloesce respects the design constraints of KV storage. For models backed purely by KV or R2, the following are not supported:
- Relationships
- Navigation properties
- Migrations
import { Model, KV, KValue, KeyParam, IncludeTree } from "cloesce/backend";
@Model()
export class Settings {
@KeyParam
settingsId: string;
@KV("settings/{settingsId}", "myNamespace")
data: KValue<unknown> | undefined;
@KV("settings/", "myNamespace")
allSettings: KValue<unknown>[];
static readonly withAll: IncludeTree<Settings> = {
data: {},
allSettings: {}
};
}
The above Model uses only KV attributes. The @KeyParam decorator indicates that the settingsId property is used to construct the KV key for the data property, using string interpolation. The @KV decorator specifies the key pattern and the KV namespace to use.
The data property is of type KValue<unknown>, which represents a value stored in KV. You can replace unknown with any serializable type, but Cloesce will not validate or instantiate the data when fetching it.
The allSettings property demonstrates how Cloesce can fetch via prefix from KV. This property will retrieve all KV entries with keys starting with settings/ and return them as an array of KValue<unknown>.
Include Trees can be used with KV Models as well to specify which properties to include when fetching data. By default, no properties are included unless specified in an Include Tree.
Note: KV properties on a Model consider a missing key as a valid state, and will not return 404 errors. Instead, the value inside of the
KValuewill be set tonull.
Note:
unknownis a special type to Cloesce designating that no validation should be performed on the data, but it is still stored and retrieved as JSON.
Alpha Note: KV Models do not yet support cache control directives and expiration times. This feature is planned for a future release.
Defining a Model with R2
Cloudflare R2 is an object storage solution similar to Amazon S3. It allows you to store and retrieve large binary objects.
Just like in KV Models, Cloesce does not support relationships, Navigation Properties, or migrations for purely R2 backed models.
Since R2 is used for storing large objects, the actual data of an R2 object is not fetched automatically when accessing an R2 property to avoid hitting Worker memory limits. Instead, only the metadata of the R2Object is retrieved. To fetch the full object data, you can use Model Methods as described in the chapter Model Methods.
import { Model, R2, R2Object, KeyParam, IncludeTree } from "cloesce/backend";
@Model()
export class MediaFile {
@KeyParam
fileName: string;
@R2("media/{fileName}.png", "myBucket")
file: R2Object | undefined;
static readonly withFile: IncludeTree<MediaFile> = {
file: {}
};
}
The MediaFile Model above is purely R2 backed. The @KeyParam decorator indicates that the fileName property is used to construct the R2 object key for the file property. The @R2 decorator specifies the key pattern and the R2 bucket to use.
The file property is of type R2Object, which represents an object stored in R2. This type provides access to metadata about the object, such as its size and content type.
Include Trees can also be used with R2 backed Models to specify which properties to include when fetching data.
Note: R2 properties on a Model consider a missing object as a valid state, and will not return 404 errors. Instead, the property will be set to
undefined.
Mixing Data Together
Cloesce allows you to combine D1, KV and R2 properties into a single Model. This provides flexibility in how you structure your data and choose the appropriate storage mechanism for each property.
import { Model, Integer, KV, KValue, R2, R2Object, KeyParam, IncludeTree } from "cloesce/backend";
@Model()
export class DataCentaur {
id: Integer;
@R2("centaurPhotos/{id}.jpg", "myBucket")
photo: R2Object;
}
@Model()
export class DataChimera {
id: Integer;
favoriteSettingsId: string;
dataCentaurId: Integer;
dataCentaur: DataCentaur | undefined;
@KV("settings/{favoriteSettingsId}", "myNamespace")
settings: KValue<unknown>;
@R2("media/{id}.png", "myBucket")
mediaFile: R2Object | undefined;
static readonly withAll: IncludeTree<DataChimera> = {
dataCentaur: {
photo: {}
},
settings: {},
mediaFile: {},
};
}
In the DataChimera Model above, we have a mix of D1, KV, and R2 properties. The id property is stored in a D1 database, while the settings property is stored in KV and the mediaFile property is stored in R2.
Mixing these storage mechanisms introduces some caveats. Whenever D1 is used in a Model, it is treated as the source of truth for that Model. This means that if the primary key does not exist in D1, the entire Model is considered non-existent, even if KV or R2 entries exist for that key.
However, if a primary key exists and the KV and R2 entries do not, Cloesce considers this a valid state and will place null or undefined in those properties respectively.
Further, using KeyParams in a Model with D1 limits the capabilities of the ORM, discussed later in this chapter. It is recommended to avoid using KeyParams in Models that also use D1 Navigation Properties.
Model Methods
Models do not define just the source, structure and infrastructure of your data, they also define the API endpoints to operate on that data.
In this section, we will explore how to define methods on Models that are exposed as API endpoints, CRUD methods generated by Cloesce, and the runtime validation that occurs when these methods are invoked.
Static and Instance Methods
A Model class in Cloesce may have both static and instance methods.
A static method is the most simple, it simply exists on the same namespace as the Model class itself. An instance method exists on an actual instance of the Model.
Methods must be decorated with a HTTP Verb such as @GET, @POST, etc. to be exposed as API endpoints.
import { Model, Integer, HttpResult } from "cloesce/backend";
@Model()
export class User {
id: Integer;
name: string;
@GET
static echo(input: string): HttpResult<string> {
if (isBadWord(input)) {
return HttpResult.fail(400, "I'm not saying that!");
}
return HttpResult.ok(`Echo: ${input}`);
}
@GET
greet(): string {
return `Hello, my name is ${this.name}.`;
}
foo() {
// Not exposed as an API method
}
}
After compilation via npx cloesce compile, the above Model will have two API endpoints:
GET /User/echo?input=yourInputCalls the staticechomethod.GET /User/{id}/greetCalls the instancegreetmethod on theUserinstance with the specifiedid.
To query these endpoints, a full generated client that matches the exact structure defined in the Model is available after compilation in .generated/client.ts.
Alpha Note: GET methods currently do not support complex types as parameters (such as other Models, arrays, etc). Only primitive types like
string,number,boolean, etc are supported. This limitation will be lifted in future releases.
CRUD Methods
When creating Models, you will find yourself writing the same CRUD (Create, Read, Update, Delete) boilerplate. To save this effort, Cloesce automatically generates standard CRUD methods if included in the Model decorator. These methods are exposed as API endpoints. Internally, they simply run the Cloesce ORM operations available via the Orm class.
import { Model, Integer } from "cloesce/backend";
@Model(["GET", "SAVE", "LIST"])
export class User {
id: Integer;
name: string;
}
The above User Model will have the following API endpoints generated automatically:
GET /User/{id}/GET?__dataSource=Fetch aUserby its primary key.POST /User/SAVE?__dataSource=Create or update aUser. TheUserdata is passed in the request body as JSON.GET /User/LIST?__dataSource=List allUserinstances.
All CRUD methods take an optional IncludeTree in the request to specify which navigation properties to include in the response. This defaults to no includes.
Note: R2 does not support CRUD methods for streaming the object body, instead it only sends the metadata.
Alpha Note: Delete is not yet supported as a generated CRUD method.
Runtime Validation
When a Model method is invoked via an API call, the Cloesce runtime automatically performs validation on the input parameters and the return value based on what it has extracted from the Model definition during compilation. This ensures that the data being passed to and from the method adheres to the expected types and constraints defined in the Model.
There are many valid types for method parameters in Cloesce, such as:
| Type | Description |
|---|---|
string | String values |
number | Floating-point numbers |
Integer | Integer values |
boolean | Boolean values (true/false) |
Date | Date and time values |
Uint8Array | Binary data |
DataSourceOf<T> | Any data source for Model type T |
unknown | JSON data of unknown structure |
DeepPartial<T> | Partial version of Model type T where anything can be missing |
| Plain Old Objects | Objects with properties of supported types |
| Model types | Custom Models (e.g., User, Post) |
| Arrays | Arrays of any supported type (e.g., string[], User[]) |
| Nullable unions | Nullable versions of any type (e.g., string | null, User | null) |
HttpResult<T> | HTTP result wrapping any supported type T |
ReadableStream | Stream of data |
Plain Old Objects
Cloesce supports the use of Plain Old Objects (POOs) as method parameters and return types. A POO is an object with properties that are of supported types. This allows developers to return or accept complex data structures without needing to define a full Model for them.
import { Model, Integer, HttpResult } from "cloesce/backend";
export class Profile {
bio: string;
age: Integer;
interests: string[];
}
@Model()
export class User {
id: Integer;
name: string;
@POST
updateProfile(profile: Profile): HttpResult<string> {
// Update the user's profile with the provided data
return HttpResult.ok("Profile updated successfully.");
}
}
Note: Plain old objects can only consist of serializable properties supported by Cloesce. They must be exported so that they can be linked. They cannot contain streams.
HttpResult
Every method response in Cloesce is converted to a HttpResult internally. This allows methods to have fine-grained control over the HTTP response, including status codes and headers.
DeepPartial
Cloesce provides a special utility type called DeepPartial<T>, which allows for the creation of objects where all properties of type T are optional, and this optionality is applied recursively to nested objects. This is particularly useful for update operations where you may only want to provide a subset of the properties of a Model.
Stream Input and Output
Cloesce supports streaming data both as input parameters and return values in Model methods. This is particularly useful for handling large files or data streams without loading everything into memory at once. Streams can be hinted to Cloesce using the ReadableStream type.
If a method parameter is of type ReadableStream, no other validation is performed on the input data. Additionally, no other parameters are allowed in the method signature when using a stream input (aside from injected dependencies, which are discussed later).
When a method returns a ReadableStream, Cloesce will return a plain Response on the client side, allowing for efficient streaming of data back to the client.
Cloesce allows a HttpResult<ReadableStream> to be returned as well, which provides the ability to set custom status codes and headers while still streaming data.
For example, to return a R2 object’s body as a stream, you can define a method like this:
import { Model, Integer, R2Object, GET, HttpResult } from "cloesce/backend";
@Model()
export class MediaFile {
id: Integer;
@R2("media/{id}.png", "myBucket")
r2Object: R2Object | undefined;
@GET
downloadFile(): HttpResult<ReadableStream> {
if (!this.r2Object) {
return HttpResult.fail(404, "File not found.");
}
// Body is never loaded into memory, just streamed!
return HttpResult.ok(this.r2Object.body);
}
}
Cloesce ORM
Alpha Note: The ORM is subject to change as new features are added.
During the hydration step of the Cloesce runtime, all of a Models data is fetched from it’s various defined sources (D1, KV, R2) and combined into a single object instance. This unified object can then be used seamlessly within your application code.
Luckily, Cloesce doesn’t keep this functionality to itself, it is made available through the Orm class in the cloesce/backend package.
Getting and Listing Models
Cloesce provides two basic methods to select a Model from D1, KV and R2:
import { Orm } from "cloesce/backend";
import { User } from "@data"
const orm = Orm.fromEnv(env);
const user = await orm.get(User, {
id: 1,
keyParams: {
myParam: "value"
},
includeTree: User.withFriends
});
// => User | undefined
const users = await orm.list(User, User.withFriends);
// => User[]
Note that the get method requires the primary key of the Model to be passed in, along with any key parameters needed to construct KV or R2 keys.
The list method simply takes an optional Include Tree to specify which navigation properties to include. This means that the list method cannot be used with Models that require key parameters for KV or R2 properties (try using prefix queries instead).
Select, Map and Hydrate
Typically, when using a relational database, you require more advanced filtering capabilities. Instead of creating a DSL for querying models (such as LINQ) or advanced libraries like Drizzle, Cloesce takes a stance that when you need to do a SQL query– write it in SQL.
However, the logic of LEFT JOINing related tables based on navigation properties can be tedious and error prone. Additionally, some way to turn the flat result set of a SQL query into JSON objects, and some way to turn those JSON objects into fully fledged Model instances with KV and R2 properties populated is needed.
The select method generates the appropriate SQL query to fetch the desired data from D1, generating joins for navigation properties based on the provided Include Tree. It also aliases the selected columns to match the object graph structure, which is useful for filtering.
Let’s create a simple set of Models to demonstrate this:
@Model()
export class Boss {
id: Integer;
persons: Person[];
static readonly withAll: IncludeTree<Boss> = {
persons: {
dogs: {},
cats: {}
}
};
}
@Model()
export class Person {
id: Integer;
bossId: Integer;
dogs: Dog[];
cats: Cat[];
}
@Model()
export class Dog {
id: Integer;
personId: Integer;
Person: Person | undefined;
}
@Model()
export class Cat {
id: Integer;
personId: Integer;
Person: Person | undefined;
}
Using the select ORM method with the Boss.withAll Include Tree will generate the following SQL:
SELECT
"Boss"."id" AS "id",
"Person_1"."id" AS "persons.id",
"Person_1"."bossId" AS "persons.bossId",
"Dog_2"."id" AS "persons.dogs.id",
"Dog_2"."personId" AS "persons.dogs.personId",
"Cat_3"."id" AS "persons.cats.id",
"Cat_3"."personId" AS "persons.cats.personId"
FROM "Boss"
LEFT JOIN "Person" AS "Person_1"
ON "Boss"."id" = "Person_1"."bossId"
LEFT JOIN "Dog" AS "Dog_2"
ON "Person_1"."id" = "Dog_2"."personId"
LEFT JOIN "Cat" AS "Cat_3"
ON "Person_1"."id" = "Cat_3"."personId"
Utilizing the aliased results in a CTE expression allows for easy filtering based on navigation properties:
const selectSql = Orm.select(User, {
includeTree: Boss.withAll
});
const query = `
WITH BossCte AS (
${selectSql}
)
SELECT * FROM BossCte WHERE
[persons.dogs.id] = 5
AND
[persons.cat.id] = 10
AND
[persons.id] = 15
`;
This SQL can be executed on a D1 instance, and the results passed to the map method to convert the flat result set into JSON objects:
const results = await d1.prepare(query).all();
const bosses = Orm.map(Boss, results, Boss.withAll);
// => Boss[]
Finally, the hydrate method can be used to take these JSON objects and convert them into fully fledged Model instances, with KV and R2 properties fetched and populated:
const orm = Orm.fromEnv(env);
const hydratedBosses = await orm.hydrate(Boss, {
base: bosses,
keyParams: {...},
includeTree: Boss.withAll
});
// => Boss[]
Note:
Orm.maprequires the input results to be in the exact aliased format generated byOrm.select. Mixing and matching with other SQL queries may fail.
Saving a Model
Cloesce combines posting and editing a Model into a single method upsert. Upsert is capable of creating or inserting complex object graphs including D1 and KV properties. R2 properties are not supported for upsert since they typically involve large binary data that is better handled separately.
import { Orm } from "cloesce/backend";
import { User } from "@data"
const orm = Orm.fromEnv(env);
const result = await orm.upsert(User, {
// id: 1, Assume User.id is an integer, we can auto-increment it
name: "New User",
friends: [
{
// Again assume Friend.id is an integer
name: "Friend 1"
},
{
id: 1, // Existing Friend
name: "My Best Friend" // Update existing Friend name
},
]
}, User.withFriends);
Upsert would then return the newly created User instance, complete with assigned primary keys and any navigation properties specified in the Include Tree, along with the newly created Friends.
Wrangler Environment
Note: Unlike some Infrastructure as Code tools for Cloudflare, Cloesce does not try to fully replace using a Wrangler configuration file. Account specific configurations settings such as binding IDs must still be managed manually.
Cloesce will search your project for a class decorated with @WranglerEnv to define the Cloudflare Workers environment tailored to your application.
A wrangler.toml file is generated during compilation based on this class. Currently, only D1 databases, R2 buckets, KV namespaces and string environment variables are supported.
Cloesce will not overwrite an existing wrangler.toml file, or any unique configurations you may have added to it. It will append any missing bindings and variables defined in the @WranglerEnv class.
An instance of the WranglerEnv is always available as a dependency to inject. See the Services chapter for more information on dependency injection.
An example WranglerEnv class is shown below:
@WranglerEnv
export class Env {
db: D1Database;
bucket: R2Bucket;
kv: KVNamespace;
someVariable: string;
}
This will compile to the following wrangler.toml file:
compatibility_date = "2025-10-02"
main = ".generated/workers.ts"
name = "cloesce"
[[d1_databases]]
binding = "db"
database_id = "replace_with_db_id"
database_name = "replace_with_db_name"
[[r2_buckets]]
binding = "bucket"
bucket_name = "replace-with-r2-bucket-name"
[[kv_namespaces]]
binding = "kv"
namespace_id = "replace_with_kv_namespace_id"
[vars]
myVariable = "default_string"
Alpha Note: Only one D1 database binding is currently supported. Future releases will allow multiple D1 bindings.
Services
Models are not the only way to write API logic in Cloesce.
Services are another core concept that allow you to encapsulate business logic and share it across your application. Services are similar to Models in that they can define methods that can be called from your API routes, but they do not have any associated data storage or schema.
Instead, Services are used to group complex related functionality together and can be injected into other parts of your application using Cloesce’s dependency injection system.
TIP: A clean design pattern for Cloesce is to use Services to encapsulate significant business logic and have Models act as thin wrappers around data storage and retrieval. This separation of concerns can lead to more maintainable and testable code.
Hello World Service
Let’s create a simple Service that returns a “Hello, World!” message.
import { Service, GET } from 'cloesce/backend';
@Service
export class HelloWorldService {
init(): HttpResult<void> | undefined {
// Optional initialization logic can go here
}
@GET
hello(): string {
return "Hello, World!";
}
}
After running npx cloesce compile, this Service will be available at the endpoint HelloWorldService/hello, and a client method will be generated for you to call it from the frontend.
Dependency Injection
To share dependencies across your Cloesce application methods, Cloesce utilizes dependency injection. By default, Cloesce provides two dependencies:
- Wrangler Environment: Access to your Cloudflare Workers environment variables.
- Request: The incoming HTTP request object.
You can access these dependencies by decorating your method parameters with the @Inject decorator on any Cloesce Model or Service method:
import { Service, GET, WranglerEnv } from 'cloesce/backend';
@WranglerEnv
class Env {
d1: D1Database;
}
@Service
export class HelloWorldService {
@GET
async hello(@Inject env: Env, @Inject request: Request): Promise<string> {
console.log("Request URL:", request.url);
const res = await env.d1.prepare("SELECT 'Hello, World!' AS message").first<{ message: string }>();
return res.message;
}
}
Unlike Models, which require all attributes to be SQL columns, KV keys, or R2 objects, Services allow attributes to be any arbitrary value, searching for them in the dependency injection context. This means you can easily inject custom services, utilities, or configurations into your Service methods as needed.
@Service
export class HelloWorldService {
env: Env;
request: Request;
foo: string;
init(): void {
this.foo = "bar";
}
@GET
async hello(): Promise<string> {
console.log("Request URL:", this.request.url);
const res = await this.env.d1.prepare("SELECT 'Hello, World!' AS message").first<{ message: string }>();
return res.message + " and foo is " + this.foo;
}
}
Services as Dependencies
Services can also be injected into other Services. They cannot be circularly dependent, but otherwise, you can freely compose Services together.
@Service
export class GreetingService {
greet(name: string): string {
return `Hello, ${name}!`;
}
}
@Service
export class HelloWorldService {
greetingService: GreetingService;
@GET
hello(name: string): string {
return this.greetingService.greet(name);
}
}
Middleware
Middleware functions are events called in between states of the Cloesce Router processing pipeline. They can be used to modify requests, exit early, or perform actions before or after certain operations.
It is important to note that the Cloesce client expects results to come exactly as they have been described in API methods. Therefore, middleware should not modify the structure of a response or change the expected output of an API method unless you are fully aware of the implications.
Custom Main Entrypoint
The most basic form of middleware is a custom main entrypoint function, which will be called for every request to your Cloesce application.
Cloesce will search your project for an exported main entrypoint. If it doesn’t appear, a default main will be generated that simply initializes the Cloesce application. The main entrypoint allows you to intercept a request before it reaches the Cloesce Router, and handle the output of the Cloesce Router as you see fit.
Below is an example of using the main entrypoint to attach CORS headers to every response:
import { CloesceApp } from "cloesce/backend";
export default async function main(
request: Request,
env: Env,
app: CloesceApp,
_ctx: ExecutionContext): Promise<Response> {
// preflight
if (request.method === "OPTIONS") {
return HttpResult.ok(200, undefined, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}).toResponse();
}
// Run Cloesce router
const result = await app.run(request, env);
// attach CORS headers
result.headers.set("Access-Control-Allow-Origin", "*");
result.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
);
result.headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
return result;
}
Note: The Cloesce Router will never throw an unhandled exception. All errors are converted into
HttpResultresponses. Therefore, there is no need to wrapapp.runin a try/catch block. 500 errors are logged by default.
Middleware Hooks
Alpha Note: Middleware hooks are likely to change significantly before a stable release.
Middleware hooks can be registered to run at specific points in the Cloesce Router processing pipeline. The available middleware hooks are:
| Hook | Description |
|---|---|
onRoute | Called when a request hits a valid route with the correct HTTP method. Service initialization occurs directly after this point, thus services will not be available. |
onNamespace | Called when a request hits a specific namespace (Model or Service). Occurs after service initialization but before request body validation. |
onMethod | Called when a request is about to invoke a specific method. Occurs after request body validation but before hydration and method execution. |
Note: Many hooks can be registered. Hooks are called in the order they are registered, per hook.
Each hook has access to the dependency injection container for the current request, allowing you to modify it as needed.
export class InjectedThing {
value: string;
}
export default async function main(
request: Request,
env: Env,
app: CloesceApp,
_ctx: ExecutionContext): Promise<Response> {
app.onNamespace(Foo, (di) => {
di.set(InjectedThing, {
value: "hello world",
});
});
app.onMethod(Foo, "blockedMethod", (_di) => {
return HttpResult.fail(401, "Blocked method");
});
return await app.run(request, env);
}
Middleware is capable of short-circuiting the request processing by returning an HttpResult directly. This is useful for implementing features like authentication. Middleware can also modify the dependency injection container for the current request, allowing you to inject custom services or data.
Alpha Note: Middleware can only inject classes into the DI container at this time. Injecting primitive values (strings, numbers, etc) is not yet supported, but a solution is planned for a future release.
Testing
Cloesce Models and Services all live as their own isolated units with no inherent connection to an incoming Worker request, making them easy to unit test.
To write tests for Cloesce that utilize any ORM features, ensure you have:
- Have ran
npx cloesce compileto generate the necessary files - Run migrations for the Models being tested
- Invoke
CloesceApp.initto initialize the Cloesce runtime
import { CloesceApp } from "cloesce/backend";
import { cidl, constructorRegistry } from "@generated/workers";
// Cloesce must be initialized before utilizing any ORM features.
// It takes in the generated Cloesce Interface Definition Language (CIDL)
// and the generated constructor registry. Both may be imported from
// "@generated/workers" as shown above.
beforeAll(() => CloesceApp.init(cidl as any, constructorRegistry));
Cloesce needs only the CIDL (generated interface definition) and Constructor Registry (linked Model, Service and Plain Old Object exports) to function be used in tests.
Since Models rely on Cloudflare Workers bindings (D1, KV, R2, etc), you will need to mock these bindings in your test environment. The best choice for this is Miniflare, which provides an in memory implementation of Cloudflare Workers runtime and bindings.
A basic Miniflare setup is shown in the template project which can be installed with:
$ npx create-cloesce my-cloesce-app
Compiler Reference
Alpha Note: As Cloesce continues to evolve, the compiler architecture and features may change. This chapter will be updated accordingly to reflect the latest design and implementation details.
This chapter provides a detailed reference for the Cloesce compiler, including its design principles, architecture and key components.
Future Vision
Cloesce is an ambitious project that aims to create a simple but powerful paradigm for building full stack applications. Several goals drive the future development of Cloesce, shaping its design and evolution. Although still in its early stages, these goals provide a clear roadmap for its growth.
Language Agnosticism
A central concept of Cloesce’s vision is full language agnosticism. Modern web development forces teams to navigate a maze of parallel ecosystems, each with its own web servers, dependency injection patterns, ORMs, migration tools, validation libraries, and routing conventions. These systems all solve the same problems, yet they do so with incompatible syntax and assumptions. Cloesce aims to collapse this redundancy by defining a single architecture that can be compiled into any target language, allowing developers to choose their preferred runtime without sacrificing consistency.
On the client side, this goal is well within reach: any language can consume REST APIs, making generated client code naturally portable. The server side, however, presents a deeper challenge. To move toward true language independence, Cloesce’s core is implemented in Rust and compiled to WebAssembly, enabling the possibility of targeting multiple server environments in the future.
The most significant obstacle today is the Cloudflare Workers platform, which treats only JavaScript and TypeScript as first-class citizens. Encouragingly, Cloudflare’s ongoing effort to support Rust based Workers through WebAssembly is now in beta and shows strong potential. As this matures, Cloesce will be able to target Rust and possibly other compiled languages without compromising its architecture.
Equally important is the evolution of WASI. With the upcoming WASI Preview 3 introducing a native async runtime, WebAssembly is rapidly becoming a viable foundation for general purpose server development. This progress directly expands the horizons of what Cloesce can support in the future, and also allows Cloesce to move the entirety of its runtime into WebAssembly, further decoupling it from any single language or platform.
Support the full set of Cloudflare Workers features
Cloesce should be the best way to develop with Cloudflare. To achieve this, developers cannot be limited in leveraging the capabilities of the Workers platform. Future versions of Cloesce will aim to enhance the full range of Workers features, including:
- Durable Objects and Web Socket API generation
- Native solution for D1 sharding
- Worker to Worker communication
- Hyperdrive + PostgreSQL support
- … and more as Cloudflare continues to expand the Workers platform.
Designed for AI Integration
With Cloesce, you write less code. Significantly less. Furthermore, the code you do write is at a high level of abstraction, focusing on only data models and business logic. This design makes Cloesce an ideal candidate for AI assisted development. Not only would creating a project require a fraction of the tokens, but the high level nature of the code means that AI can more easily understand the intent and structure of the application.
By its stable release, simply asking an AI agent to “Create Twitter with Cloesce” should yield a complete, functional application ready to deploy to Cloudflare Worker in a record low token count.
Architecture Overview
We can break down the Cloesce architecture into three components, each with their own subdomains: the Frontend, Generator and the Migrations Engine.
Frontend
The Frontend layer of Cloesce encompasses all components responsible for interfacing with the user’s project and extracting high level language into the Cloesce Interface Definition Language (CIDL). It serves as the both the entrypoint for compilation and the runtime environment for generated code.
IDL Extraction
A key design choice when building Cloesce was to not force users to write their Models in a separate IDL or DSL as seen in tools like gRPC and Prisma.
Instead, we opted to have the Cloesce compiler utilize the source languages AST to extract Model definitions directly from the user’s source code. This allows users to define their Models using familiar syntax and semantics, while still benefiting from the powerful features of Cloesce. Of course, this means we must write an extractor for each supported frontend language. Currently, the only supported language is TypeScript.
The Extractor portion of the compiler is responsible for scanning the user’s source files (marked with .cloesce.<lang>) and identifying Model definitions through stub preprocessor directives. Extraction does not perform any semantic analysis, it simply extracts the Model definitions and their properties into an intermediate representation (the CIDL).
The CIDL describes a full stack project. Every Model definition, Service, API endpoint and Wrangler binding is stored in the CIDL. Currently, this representation is serialized as JSON, but in the future we may explore other formats such as Protocol Buffers or FlatBuffers for better performance and extensibility.
At the end of CIDL Extraction, a cidl.pre.json file is produced to semantically validated by the Generator.
Runtime
Beyond extraction, the Frontend layer also includes the runtime environment for workers. Originally, we considered generating entirely standalone code for the Workers, but a shift to instead interpret the CIDL at runtime as if it was the program text allowed us to greatly reduce the amount of generated code, add tests and improve maintainability. Each supported frontend language has its own runtime implementation that can interpret the CIDL using simple state machine logic. Moving as much of the runtime logic into WebAssembly as possible helps portability to other languages in the future.
The runtime consists of two components: the Router and the ORM. The Router is currently written entirely in TypeScript, while the ORM compiles to WebAssembly from Rust.
Depending on the context, the CIDL may be referred to as the “Abstract Syntax Tree”, the “Cloesce Interface Definition Language”, or during the runtime just “metadata” when a particular node is being referenced. All of these labels are accurate– it’s a versatile structure!
Router
The Cloesce Router is responsible for handling incoming HTTP requests, matching them to an API endpoint defined in the CIDL, validating request parameters and body, hydrating data from the ORM and dispatching to a user defined method on a Model or Service. Along the way, the Router calls middleware functions defined in the CIDL. Although each middleware function can produce undefined behavior, each state in the Router is well defined and can produce only a corresponding failure state or success state. This makes reasoning about the Router’s behavior straightforward.
ORM
The Cloesce ORM is responsible for fetching and updating data stored in SQL, KV and R2 according to the Model definitions and Include Trees passed. The majority of the ORM is written in Rust, however some portions are written in TypeScript such as KV and R2 hydration logic.
Generator
After being passed the pre.cidl.json file from the Frontend, the Generator performs semantic analysis on the CIDL and Wrangler configuration to ensure that the project is valid. This includes checking for translatable SQLite types, sorting Models and Services topologically, validating API endpoints and more. If any errors are found, the Generator will output them to the user and halt compilation.
After semantic analysis is complete, the Generator produces the final cidl.json file which is then used to generate code for the worker runtime and client code generation. The generator will augment the CIDL with additional information like hashes for migrations and CRUD API endpoints for Models and Services.
To make the Generator easily available to the frontend, it is written entirely in Rust and compiled to WebAssembly. This allows frontend languages to easily call into the Generator without needing to write language specific bindings. In the future, we may explore compiling native binaries for better performance, but WASM reduces the complexity of distributing multiple binaries for different platforms.
Migrations Engine
After a successful compilation, the Migrations Engine is used to generate database migrations from changes in the Model definitions, utilizing the Merkle-Tree hashes the Generator added to the CIDL.
The engine can sometimes encounter problems it does not know the solution to, such as when a Model is renamed. In these cases, the engine will prompt the user with options on how to proceed (such as generating a rename migration or creating a new Model). This interactive process ensures that the generated migrations align with the user’s intent.
The Migrations Engine outputs SQLite files intended to be applied to D1 databases using the Wrangler CLI. It is written entirely in Rust and comes with the Generator as a single WebAssembly module for easy distribution.