Design a data model and the PHP web service that backs it already exists — REST API, MCP tools and migrations included.
See the codeThe authoring environment of the Sabatier stack.

An attribute renamed in the editor, saved, and read back over HTTP under its new name with the values still in place. One take, no cut: there was no migration step in between.
You declare routes and controllers in every project you build. They can — and should — be inferred, because they are always the same. And when you later want to expose that same data to an agent, you declare all of it again, permissions included.
Singularity is where you stop doing that. Design the data model — in a visual
editor, or by describing it to an agent over MCP — and the backend is already
there. Not scaffolding you replace once the real work starts, but what the
application is still standing on when it has grown. Singularity itself is the
proof: it was modeled in its own editor, its model saved as
Resources/Singularity.mom, the classes in
src/Model/ generated from it, and its own directory layout is
exactly what it scaffolds for a new project.
What comes with the model, without being written:
Nor do you write the project around it. Creating one generates the whole bundle — Info.plist, composer.json, HTTP and CLI entry points, the application delegate, the .mom model, and a .env already carrying the database settings and, if you asked for JWT, a freshly generated signing key. The managed-object subclasses are generated from the model on demand, from the editor or over MCP.
No route, no controller, no serializer, no tool definition, no migration file, no bootstrap. One declaration, every surface.
A bookstore modelled from a sentence, the REST and MCP surfaces answering for it, and the rename above in full.
What you generate is a Sabatier Service backend, and how much code you write on top of it is entirely up to you. The same design tool spans the whole range:
Singularity itself sits at the far end of that range: a full application built on its own generated backend. One generator, the whole spectrum.

That claim, on screen: this is Singularity's own model open in its own editor, on the entity named Model. Every relationship carries its inverse, and the inspector on the right is where an entity declares the class it generates, how it versions, and who may read it.

The same model as a graph. Layouts are draggable and persisted with the project.
Model two entities — Author and Book, one relationship between them — and generate the project. Nothing else is written. This is the whole of its src/:
src/
└── Delegate.php
The entities are already served:
curl -X POST http://127.0.0.1:8001/Author \
-H "Content-Type: application/json" \
-d '{"name":"Ursula K. Le Guin","country":"US"}'
{"objectID":1,"entityName":"Author","name":"Ursula K. Le Guin","country":"US","creationDate":"2026-09-22 11:21:45"}
201 Created. The creationDate was neither sent nor defaulted by hand: an attribute the model declares non-optional never arrives null, so a date becomes the current time and a UUID is generated. The model states the guarantee and the object keeps it.
Relate a new object to one that already exists by naming it — an objectID and at least one attribute:
curl -X POST http://127.0.0.1:8001/Book \
-H "Content-Type: application/json" \
-d '{"title":"The Left Hand of Darkness","isbn":"9780441478125","author":{"objectID":1,"name":"Ursula K. Le Guin"}}'
And ask for the shape you want back, rather than the one an endpoint decided for you:
curl http://127.0.0.1:8001/Book \
-H 'Serialization: {"title":true,"isbn":true,"author":{"name":true,"country":true}}'
[{"objectID":1,"entityName":"Book","title":"The Left Hand of Darkness","isbn":"9780441478125",
"author":{"objectID":1,"entityName":"Author","name":"Ursula K. Le Guin","country":"US"}}]
objectID and entityName come back at every level whether or not the
projection asks for them: they are what identifies a row, and a response that
omits them cannot be used to update one.
The same model answers an MCP client: describe_model returns the entities with their attributes and relationships, and fetch returns the rows above. No tool definitions were written either.
Then rename country to nationality in the editor and save. The next request returns the new name with the values still in place — no migration file, no migration step.
Every app generated by Singularity is, by definition, a Service application running on CoreData, grounded in Foundation. The point is not merely to generate files, but to lock the architecture in:
Most frameworks treat persistence as plumbing and state as transient. Singularity treats the model as the center of gravity, so the rest of the system can be honest: objects keep their identity, changes are tracked rather than inferred, the lifecycle is explicit, and an entity is reachable over HTTP and MCP the moment it exists in the model — no wiring, no migration scripts. Create a project, model your system, save, and CoreData migrates structure and data automatically.
.mom model and data automatically; no CLI ceremony, no hand-written migrations.json, gettext, intl, redis127.0.0.1 (default port)mod_rewrite, serving the project from the document root (routing is handled by .htaccess). On a vhost, also set Options -MultiViews.../Foundation, ../CoreData, ../ServiceThe PHP backend is served at http://localhost:8001/.
Copy the two example files and fill in what your installation needs:
cp .env.example .env
cp .htaccess.example .htaccess
.env carries the database name, host and credentials; .env.example documents every variable with its default. The database itself does not need to exist yet: CoreData creates the schema named in SQL_SCHEMA_NAME on first run, along with its tables, constraints and correlation tables. The credentials you supply must be allowed to create it.
composer install
npm install
npm run dev # Vite dev server on port 5173 (hot reload)
npm run build # production build to Build/
For hot-reload during development, set VITE_DEV_SERVER=http://127.0.0.1:5173 in .env. Without it, assets are loaded from the production build via Build/.vite/manifest.json.
cd Electron
npm install
npm start # electron-forge dev mode
npm run make # package for distribution
Electron does not serve the app — it wraps the running PHP server in a native window.
Singularity is a Service application. Routing is attribute-based: the framework reflects on PHP attributes to build routes, so defining a controller is defining a route. Views are server-rendered with Latte and hydrated by a Vite/TypeScript frontend.
The object graph mirrors the domain it edits — because it is the domain it edits. The managed objects in src/Model/ were designed as entities in Singularity's own editor, then generated:
Project → Model → Entity → Attribute / Relationship / FetchedProperty
Entity also groups: FetchIndex, UniquenessConstraint
Model also groups: FetchRequestTemplate, Configuration, CompositeType
The AI Copilot conversations persist under the same model as Conversation → Message → ToolCall.
For the full architectural reference — project scaffolding, the code-generation pipeline, model versions and migration, the frontend, and the AI Copilot — see ARCHITECTURE.md.
Inference is only tolerable if you can watch it work. Both ends of a query are observable, and both are off by default — you pay nothing until you ask.
How a predicate was evaluated. Predicate::$debugDefault turns on an evaluation trace. Each line is indented to the depth it was reached at, so a compound predicate prints as the tree it actually is — which is how you find out that a OR b AND c did not group the way you assumed:
Predicate::$debugDefault = true;
Predicate::$debugHandler = fn(string $line) => fwrite(STDERR, "$line\n");
$debugHandler is optional: unset, the trace goes to error_log(). Assign a closure to send it wherever a test or a console can read it.
What SQL it became. SQLCore::$debugLevel is a scale, not a switch:
| Level | Shows |
|---|---|
none | nothing (default) |
rawSQL | the generated statement |
sqlWithParams | the statement with its bound values |
prettyFormatSQL | the statement, formatted |
includeResults | what it returned |
analyzeJSON | the engine's execution plan |
SQLCore::$debugColorOutputDefault colorizes it for a terminal. Both are marked @internal — they are a debugging aid, not a stable API, and the level names may change.
Together they answer the question an abstraction usually cannot: what did it actually do? The predicate you wrote, the tree it parsed into, the SQL it lowered to, the parameters it bound, the rows it got back, and the plan the engine chose.
Built for engineers who want applications with structure, memory, and identity — and an editor that is itself proof the stack delivers them.
PHP
43.6%
Latte
27.6%
TypeScript
20.0%
SCSS
8.4%
Design a data model and the PHP web service that backs it already exists — REST API, MCP tools and migrations included.
See the codeThe authoring environment of the Sabatier stack.

An attribute renamed in the editor, saved, and read back over HTTP under its new name with the values still in place. One take, no cut: there was no migration step in between.
You declare routes and controllers in every project you build. They can — and should — be inferred, because they are always the same. And when you later want to expose that same data to an agent, you declare all of it again, permissions included.
Singularity is where you stop doing that. Design the data model — in a visual
editor, or by describing it to an agent over MCP — and the backend is already
there. Not scaffolding you replace once the real work starts, but what the
application is still standing on when it has grown. Singularity itself is the
proof: it was modeled in its own editor, its model saved as
Resources/Singularity.mom, the classes in
src/Model/ generated from it, and its own directory layout is
exactly what it scaffolds for a new project.
What comes with the model, without being written:
Nor do you write the project around it. Creating one generates the whole bundle — Info.plist, composer.json, HTTP and CLI entry points, the application delegate, the .mom model, and a .env already carrying the database settings and, if you asked for JWT, a freshly generated signing key. The managed-object subclasses are generated from the model on demand, from the editor or over MCP.
No route, no controller, no serializer, no tool definition, no migration file, no bootstrap. One declaration, every surface.
A bookstore modelled from a sentence, the REST and MCP surfaces answering for it, and the rename above in full.
What you generate is a Sabatier Service backend, and how much code you write on top of it is entirely up to you. The same design tool spans the whole range:
Singularity itself sits at the far end of that range: a full application built on its own generated backend. One generator, the whole spectrum.

That claim, on screen: this is Singularity's own model open in its own editor, on the entity named Model. Every relationship carries its inverse, and the inspector on the right is where an entity declares the class it generates, how it versions, and who may read it.

The same model as a graph. Layouts are draggable and persisted with the project.
Model two entities — Author and Book, one relationship between them — and generate the project. Nothing else is written. This is the whole of its src/:
src/
└── Delegate.php
The entities are already served:
curl -X POST http://127.0.0.1:8001/Author \
-H "Content-Type: application/json" \
-d '{"name":"Ursula K. Le Guin","country":"US"}'
{"objectID":1,"entityName":"Author","name":"Ursula K. Le Guin","country":"US","creationDate":"2026-09-22 11:21:45"}
201 Created. The creationDate was neither sent nor defaulted by hand: an attribute the model declares non-optional never arrives null, so a date becomes the current time and a UUID is generated. The model states the guarantee and the object keeps it.
Relate a new object to one that already exists by naming it — an objectID and at least one attribute:
curl -X POST http://127.0.0.1:8001/Book \
-H "Content-Type: application/json" \
-d '{"title":"The Left Hand of Darkness","isbn":"9780441478125","author":{"objectID":1,"name":"Ursula K. Le Guin"}}'
And ask for the shape you want back, rather than the one an endpoint decided for you:
curl http://127.0.0.1:8001/Book \
-H 'Serialization: {"title":true,"isbn":true,"author":{"name":true,"country":true}}'
[{"objectID":1,"entityName":"Book","title":"The Left Hand of Darkness","isbn":"9780441478125",
"author":{"objectID":1,"entityName":"Author","name":"Ursula K. Le Guin","country":"US"}}]
objectID and entityName come back at every level whether or not the
projection asks for them: they are what identifies a row, and a response that
omits them cannot be used to update one.
The same model answers an MCP client: describe_model returns the entities with their attributes and relationships, and fetch returns the rows above. No tool definitions were written either.
Then rename country to nationality in the editor and save. The next request returns the new name with the values still in place — no migration file, no migration step.
Every app generated by Singularity is, by definition, a Service application running on CoreData, grounded in Foundation. The point is not merely to generate files, but to lock the architecture in:
Most frameworks treat persistence as plumbing and state as transient. Singularity treats the model as the center of gravity, so the rest of the system can be honest: objects keep their identity, changes are tracked rather than inferred, the lifecycle is explicit, and an entity is reachable over HTTP and MCP the moment it exists in the model — no wiring, no migration scripts. Create a project, model your system, save, and CoreData migrates structure and data automatically.
.mom model and data automatically; no CLI ceremony, no hand-written migrations.json, gettext, intl, redis127.0.0.1 (default port)mod_rewrite, serving the project from the document root (routing is handled by .htaccess). On a vhost, also set Options -MultiViews.../Foundation, ../CoreData, ../ServiceThe PHP backend is served at http://localhost:8001/.
Copy the two example files and fill in what your installation needs:
cp .env.example .env
cp .htaccess.example .htaccess
.env carries the database name, host and credentials; .env.example documents every variable with its default. The database itself does not need to exist yet: CoreData creates the schema named in SQL_SCHEMA_NAME on first run, along with its tables, constraints and correlation tables. The credentials you supply must be allowed to create it.
composer install
npm install
npm run dev # Vite dev server on port 5173 (hot reload)
npm run build # production build to Build/
For hot-reload during development, set VITE_DEV_SERVER=http://127.0.0.1:5173 in .env. Without it, assets are loaded from the production build via Build/.vite/manifest.json.
cd Electron
npm install
npm start # electron-forge dev mode
npm run make # package for distribution
Electron does not serve the app — it wraps the running PHP server in a native window.
Singularity is a Service application. Routing is attribute-based: the framework reflects on PHP attributes to build routes, so defining a controller is defining a route. Views are server-rendered with Latte and hydrated by a Vite/TypeScript frontend.
The object graph mirrors the domain it edits — because it is the domain it edits. The managed objects in src/Model/ were designed as entities in Singularity's own editor, then generated:
Project → Model → Entity → Attribute / Relationship / FetchedProperty
Entity also groups: FetchIndex, UniquenessConstraint
Model also groups: FetchRequestTemplate, Configuration, CompositeType
The AI Copilot conversations persist under the same model as Conversation → Message → ToolCall.
For the full architectural reference — project scaffolding, the code-generation pipeline, model versions and migration, the frontend, and the AI Copilot — see ARCHITECTURE.md.
Inference is only tolerable if you can watch it work. Both ends of a query are observable, and both are off by default — you pay nothing until you ask.
How a predicate was evaluated. Predicate::$debugDefault turns on an evaluation trace. Each line is indented to the depth it was reached at, so a compound predicate prints as the tree it actually is — which is how you find out that a OR b AND c did not group the way you assumed:
Predicate::$debugDefault = true;
Predicate::$debugHandler = fn(string $line) => fwrite(STDERR, "$line\n");
$debugHandler is optional: unset, the trace goes to error_log(). Assign a closure to send it wherever a test or a console can read it.
What SQL it became. SQLCore::$debugLevel is a scale, not a switch:
| Level | Shows |
|---|---|
none | nothing (default) |
rawSQL | the generated statement |
sqlWithParams | the statement with its bound values |
prettyFormatSQL | the statement, formatted |
includeResults | what it returned |
analyzeJSON | the engine's execution plan |
SQLCore::$debugColorOutputDefault colorizes it for a terminal. Both are marked @internal — they are a debugging aid, not a stable API, and the level names may change.
Together they answer the question an abstraction usually cannot: what did it actually do? The predicate you wrote, the tree it parsed into, the SQL it lowered to, the parameters it bound, the rows it got back, and the plan the engine chose.
Built for engineers who want applications with structure, memory, and identity — and an editor that is itself proof the stack delivers them.
PHP
43.6%
Latte
27.6%
TypeScript
20.0%
SCSS
8.4%