simfinity.js
GraphQL APIs for PostgreSQL and MongoDB, with optional MCP tools generated from selected schema operations.
Simfinity.js is an Apache-2.0 Node.js library that generates GraphQL APIs from object type definitions, with PostgreSQL and MongoDB/Mongoose adapters. Its optional, database-independent @simtlix/simfinity-mcp package exposes selected operations as MCP tools over stdio or Streamable HTTP; applications provide authentication and trusted context. Maintained by Simtlix and contributors; submitted by a contributor.
- Generate queries, CRUD mutations, relationships, filters and pagination from GraphQL object types
- Use PostgreSQL 15+ or MongoDB/Mongoose adapters over a shared driver-free core
- Expose selected GraphQL queries and mutations as MCP tools with the optional MCP package
- Run MCP transports over stdio or Streamable HTTP
- Customize application logic with hooks and authorization rules
README
View on GitHub ↗
Simfinity.js
A Node.js framework that turns GraphQL object types into generated queries, mutations, relationships, and database storage. Use the established MongoDB/Mongoose facade or the PostgreSQL 15+ facade with real tables and foreign keys.
Read the documentation website: start with the quick start, explore the guides, or consult the API reference. The website source is in docs/.
For a complete application, run the Barber examples: independent MongoDB and PostgreSQL backends with one shared Next.js frontend, synthetic demo data, Docker setup, and a dedicated CI workflow. Both consume released Simfinity 3.2.0 packages from npm.
Run the documentation website locally with Node.js 22+:
npm run docs:install
npm run docs:dev
For builds and hosting, see the website maintainer guide. The website documents the current source; some older examples later in this README retain historical conventions.
Documentation for both databases: The public website now covers MongoDB and PostgreSQL, including shared APIs, relationships, generated FKs, scopes, and MCP. Both adapters are available in v3.2.0 on npm. Follow the quick starts for installation, or download the runnable starters and verified release archives.
📑 Table of Contents
- Features
- Installation
- PostgreSQL support
- Quick Start
- Core Concepts
- Basic Usage
- Relationships
- Validations
- State Machines
- Controllers & Lifecycle Hooks
- Query Scope
- Authorization
- Middlewares
- Advanced Features
- MCP Generation
- Aggregation Queries
- Complete Example
- Resources
- License
- Contributing
✨ Features
- Automatic Schema Generation: Define your object model, and Simfinity.js generates all queries and mutations
- MongoDB or PostgreSQL: Choose the database facade once during application startup
- Powerful Querying: Typed filters, nested paths, pagination, sorting, and aggregations across the supported contract
- Aggregation Queries: Built-in support for GROUP BY queries with aggregation operations (SUM, COUNT, AVG, MIN, MAX)
- Auto-Generated Resolvers: Automatically generates resolve methods for relationship fields
- Automatic Index Creation: Generates MongoDB indexes for ObjectId fields and single references, including leaves inside embedded objects and embedded arrays; see the index reference
- Business Logic: Implement business logic and domain validations declaratively
- State Machines: Built-in support for declarative state machine workflows
- Lifecycle Hooks: Controller methods for granular control over operations
- Custom Validation: Field-level and type-level custom validations
- Relationship Management: Support for embedded and referenced relationships
- Authorization: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support
📦 Installation
npm install mongoose@^8.16.2 graphql@^16.11.0 @simtlix/simfinity-js@3.2.0
Prerequisites: Simfinity.js requires mongoose and graphql as peer dependencies.
PostgreSQL support
Version 3.2.0 releases @simtlix/simfinity-core, @simtlix/simfinity-mcp, @simtlix/simfinity-postgres, and the MongoDB facade in lockstep. Install the selected adapter from npm; shared dependencies resolve automatically. PostgreSQL runs the shared GraphQL query/mutation engine, including scopes, controllers, validators, state transitions and nested writes. It generates and validates tables, indexes, and real foreign keys, including inverse relations, explicit many-to-many linking entities, and references inside embedded objects. PostgreSQL installation does not pull Mongoose, MongoDB, or MCP dependencies.
All publishable libraries live under packages/:
| Directory | npm package |
|---|---|
packages/core |
@simtlix/simfinity-core |
packages/mongodb |
@simtlix/simfinity-js |
packages/postgres |
@simtlix/simfinity-postgres |
packages/mcp |
@simtlix/simfinity-mcp |
The repository root is a private npm workspace for shared tests, documentation and release tooling. MongoDB retains its existing package name, public API and deep imports such as @simtlix/simfinity-js/src/auth/rules.js. Run development commands from the root; pack MongoDB with npm pack --workspace @simtlix/simfinity-js or use the release scripts to pack all four libraries.
Enum filters resolve member names first, then declared internal values by strict equality, on both backends. For example, with ONE: { value: 'TWO' } and TWO: { value: 'two' }, filter "TWO" selects member TWO. Numeric internal values require numbers, not numeric strings. This applies to scalar lists, embedded/reference leaves and state filters across EQ, NE, LT, LTE, GT, GTE, BTW, IN and NIN. LIKE accepts string fields only. PostgreSQL writes and state guards continue to use internal enum values.
The shared runtime preserves the v3.1 contract: generated relationships run target middleware/scopes with protected identity and parent filters; nested mutations enforce child middleware and persisted ownership; standalone saveObject() wraps the complete workflow in a transaction. Accepted empty strings are preserved. Both facades expose configureQueryLimits() for bounded pagination.
Choose the backend at application setup. The existing package continues to use MongoDB; PostgreSQL uses createPostgres({ pool, schema }), the same connect(null, Type, ...)/createSchema() signatures, and an awaited initializeDatabase() before serving requests. See the canonical PostgreSQL quick start, detailed storage reference, and compatibility contract before adopting this version.
Both database facades expose the same auth, validators, scalars, and plugins helper objects. PostgreSQL keeps MCP optional; install the database-independent integration and its transport SDK only when needed:
npm install @simtlix/simfinity-mcp@3.2.0 @modelcontextprotocol/sdk@^1.13.0
Import generateMCPTools, createMCPServer, or the transport helpers from @simtlix/simfinity-mcp and pass the schema returned by createPostgres().createSchema().
Compatibility tests run the same GraphQL schemas and query corpus against both databases. IDs use UUIDs on PostgreSQL, native model/session APIs differ, and some mappings remain explicitly unsupported, including whole embedded-object uniqueness and whole-object sorting/grouping. Nested embedded scalar-list filters/sorts, ragged array group keys and array aggregate facts now match the supported Mongo query contract. Native embedded writes retain explicit nulls, apply descendant list defaults and minimize empty inline objects; historical Date parameters use UTC. PostgreSQL enforces scalar/list multikey uniqueness within embedded trees through typed owner-key tables, validates JSONB embedded shapes, and checks owned marker/row consistency with deferred database triggers. MongoDB fixes also cover list nullability wrappers, scalar-ID inverse relations, inverse aggregation paths, embedded array replacement after clearing, and isolation of mutation input across transaction retries. Existing API signatures remain unchanged.
🚀 Quick Start
The complete quick start includes MongoDB setup, installation, and working create, read, update, and delete operations. Simfinity uses ES modules and named exports.
npm install @simtlix/simfinity-js graphql@^16.11.0 mongoose@^8.16.2 graphql-yoga@^5
Set "type": "module" in your application's package.json, then create server.js:
import { createServer } from 'node:http';
import { GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql';
import { createYoga } from 'graphql-yoga';
import mongoose from 'mongoose';
import * as simfinity from '@simtlix/simfinity-js';
const SerieType = new GraphQLObjectType({
name: 'Serie',
fields: {
id: { type: GraphQLID },
name: { type: new GraphQLNonNull(GraphQLString) },
},
});
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/series?replicaSet=rs0&directConnection=true');
simfinity.connect(null, SerieType, 'serie', 'series');
const schema = simfinity.createSchema();
const yoga = createYoga({ schema });
createServer(yoga).listen(4000, '127.0.0.1', () => {
console.log('GraphQL ready at http://127.0.0.1:4000/graphql');
});
Generated mutations require a MongoDB replica set or sharded cluster. Follow the local MongoDB setup, then run node server.js and open http://127.0.0.1:4000/graphql:
mutation {
addserie(input: { name: "The Expanse" }) {
id
name
}
}
query {
series {
id
name
}
}
For a full working application, see the Barber examples, with MongoDB and PostgreSQL backends, a shared frontend, relationships, state machines, controllers, and authorization.
🔧 Core Concepts
Connecting Models
The simfinity.connect() method links your GraphQL types to Simfinity's automatic schema generation:
simfinity.connect(
mongooseModel, // Optional: Custom Mongoose model (null for auto-generation)
graphQLType, // Required: Your GraphQLObjectType
singularEndpointName, // Required: Singular name for mutations (e.g., 'book')
pluralEndpointName, // Required: Plural name for queries (e.g., 'books')
controller, // Optional: Controller with lifecycle hooks
onModelCreated, // Optional: Callback when Mongoose model is created
stateMachine // Optional: State machine configuration
);
Creating Schemas
Generate your complete GraphQL schema with optional type filtering:
const schema = simfinity.createSchema(
includedQueryTypes, // Optional: Array of types to include in queries
includedMutationTypes, // Optional: Array of types to include in mutations
includedCustomMutations // Optional: Array of custom mutations to include
);
Importing Simfinity initializes its global __Field.extensions introspection field safely whether GraphQL's fields have already been materialized or a schema already exists. Repeated module evaluation with the same GraphQL peer reuses the existing extension field and metadata types. Application field metadata is preserved.
The extension remains shared by every schema using that GraphQL peer; Simfinity's type and middleware registries belong to each runtime instance. Schemas constructed after import include FieldExtensionsType and RelationType in their type maps. Earlier schemas keep their original type maps: ordinary operations and direct metadata selections work, but named fragments on the new metadata types require a schema constructed after import.
Schema-cloning tools remain unsupported. In a process that has imported Simfinity, buildClientSchema() on a post-import schema's introspection result can also fail with duplicate metadata type names. Use Envelop plugins and in-place resolver wrapping; see the introspection metadata reference.
Global Configuration
// Prevent automatic MongoDB collection creation (useful for testing)
simfinity.preventCreatingCollection(true);
📋 Basic Usage
Automatic Query Generation
Simfinity automatically generates queries for each connected type:
// For a BookType, you get:
// - book(id: ID): Book - Get single book by ID
// - books(...filters): [Book] - Get filtered list of books
Automatic Mutation Generation
Simfinity automatically generates mutations for each connected type:
// For a BookType, you get:
// - addBook(input: BookInput): Book
// - updateBook(input: BookInputForUpdate): Book
// - deleteBook(id: ID): Book
Creation inputs retain required scalar, enum, embedded-object, and list fields. Update inputs remove the outer non-null wrapper so fields can be omitted; the entity id remains required for GraphQLID and GraphQLID!. Other ID fields remain optional on update. List item nullability is preserved:
| Output field | Create input | Update input |
|---|---|---|
[String] |
[String] |
[String] |
[String]! |
[String]! |
[String] |
[String!] |
[String!] |
[String!] |
[String!]! |
[String!]! |
[String!] |
The same wrapper handling applies to enum lists, supported custom scalar lists, and embedded lists (using their nested input types). Referenced collections keep the added / updated / deleted input object: a required collection requires that object on create, and non-null object items make added and updated items non-null. Nullable collection-operation items are ignored. Use deleted to delete children.
Empty strings, false, 0, and empty arrays are persisted after validators accept them. Omitting an update field leaves its value unchanged. Explicit null clears a nullable scalar, embedded field, or single-object reference; references clear the actual stored connectionField, or the GraphQL field name when no override is configured. Multiple nullable fields can be cleared in one update. An explicit null for an originally non-null field leaves its stored value unchanged; use an update validator to reject that input if needed.
Filtering and Querying
Every relationship or embedded terms entry is combined with AND, including repeated paths such as age GTE 18 and age LTE 30. These conditions remain ANDed with top-level logical groups and scope filters. Filter and list-sort paths must resolve to declared scalar or enum fields. id paths refer to the stored _id; comparisons use the connected Mongoose model's schema, so supplied string or numeric ID models retain their identifier representation.
Filter values must use the field's JSON scalar type. IN and NIN require flat lists, BTW requires exactly two non-null bounds, and LIKE requires a string search fragment. Literal objects, nested lists, null list elements, invalid operators, unknown paths, and malformed groups are rejected with a structured 400 error instead of being ignored. Explicit null remains supported by EQ and NE. Enum names or declared internal enum values are converted to the stored representation; state-machine state filters preserve stored state names. Date filters convert valid date values without mutating the query input. Validated string scalars support partial LIKE searches, and range filters use their base scalar type rather than the field's create/update validation constraints.
Query with powerful filtering options:
query {
books(
title: { operator: LIKE, value: "Galaxy" }
author: { operator: EQ, value: "Douglas Adams" }
pagination: { page: 1, size: 10, count: true }
sort: { terms: [{ field: "title", order: ASC }] }
) {
id
title
author
}
}
Available Operators
EQ- EqualNE- Not equalGT- Greater thanLT- Less thanGTE- Greater than or equalLTE- Less than or equalLIKE- Pattern matchingIN- In arrayNIN- Not in arrayBTW- Between two values
Logical Filters (AND / OR)
By default, all field-level filters are combined with implicit AND logic. For complex conditions requiring OR logic or nested combinations, use the AND and OR query arguments.
Simple OR
Return books in either the Sci-Fi or Fantasy category:
query {
books(
OR: [
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
]
) {
id
title
category
}
}
Flat Filters Combined with OR
Flat field filters are always ANDed at the top level, making them ideal for scope/security conditions that cannot be bypassed by user OR logic:
query {
books(
rating: { operator: GTE, value: 7.0 }
OR: [
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
]
) {
id
title
}
}
This translates to: rating >= 7.0 AND (category = "Sci-Fi" OR category = "Fantasy").
Nested AND inside OR
query {
books(
OR: [
{ AND: [
{ conditions: [{ field: "rating", operator: GTE, value: 9.0 }] }
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
]}
{ AND: [
{ conditions: [{ field: "rating", operator: GTE, value: 8.0 }] }
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
]}
]
) {
id
title
}
}
This translates to: (rating >= 9.0 AND category = "Sci-Fi") OR (rating >= 8.0 AND category = "Fantasy").
Filtering on Relationships within AND/OR
Use the path parameter to filter on related entity fields:
query {
books(
OR: [
{ conditions: [{ field: "author", path: "name", operator: LIKE, value: "Adams" }] }
{ conditions: [{ field: "author", path: "name", operator: LIKE, value: "Pratchett" }] }
]
) {
id
title
author { name }
}
}
As an equivalent shorthand, you can pass the full dotted path in field and omit path:
query {
books(
OR: [
{ conditions: [{ field: "author.name", operator: LIKE, value: "Adams" }] }
{ conditions: [{ field: "author.name", operator: LIKE, value: "Pratchett" }] }
]
) {
id
title
author { name }
}
}
Both forms produce the same query. Multi-segment paths work too — e.g. field: "author.country.code" is equivalent to field: "author", path: "country.code". Supplying both a dotted field and a separate path is rejected as ambiguous.
Mixing Flat Filters with AND/OR
You can freely combine the existing flat filter syntax with AND/OR groups. Flat filters and AND groups are all ANDed together at the top level:
query {
books(
rating: { operator: GTE, value: 7.0 }
author: { terms: [{ path: "country", operator: EQ, value: "UK" }] }
OR: [
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
]
) {
id
title
rating
category
author { name country }
}
}
This translates to: rating >= 7.0 AND author.country = "UK" AND (category = "Sci-Fi" OR category = "Fantasy"). The flat field filters (rating, author) use the existing syntax while the OR group uses the new QLFilterGroup syntax.
You can also combine flat filters with explicit AND groups for more complex logic:
query {
books(
rating: { operator: GTE, value: 5.0 }
AND: [
{
OR: [
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
]
}
{
OR: [
{ conditions: [{ field: "author", path: "country", operator: EQ, value: "UK" }] }
{ conditions: [{ field: "author", path: "country", operator: EQ, value: "US" }] }
]
}
]
) {
id
title
}
}
This translates to: rating >= 5.0 AND (category = "Sci-Fi" OR category = "Fantasy") AND (author.country = "UK" OR author.country = "US").
Collection Filtering with AND/OR
AND/OR filters are also available on collection fields (one-to-many relationships). The auto-generated resolvers for collection fields support the same AND and OR arguments:
query {
series {
seasons(
OR: [
{ conditions: [{ field: "year", operator: EQ, value: 2020 }] }
{ conditions: [{ field: "year", operator: EQ, value: 2021 }] }
]
) {
number
year
}
}
}
You can mix flat collection filters with AND/OR:
query {
series {
seasons(
number: { operator: GT, value: 1 }
OR: [
{ conditions: [{ field: "year", operator: EQ, value: 2020 }] }
{
AND: [
{ conditions: [{ field: "year", operator: GTE, value: 2022 }] }
{ conditions: [
# Equivalent to: { field: "episodes", path: "name", ... }
{ field: "episodes.name", operator: LIKE, value: "Final" }
] }
]
}
]
) {
number
year
episodes { name }
}
}
}
This translates to: number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.name LIKE "Final")).
Filter Types Reference
| Type | Fields | Description |
|---|---|---|
QLFilterGroup |
AND: [QLFilterGroup], OR: [QLFilterGroup], conditions: [QLFilterCondition] |
Recursive logical group |
QLFilterCondition |
field: String!, operator: QLOperator, value: QLValue, path: String |
Individual filter condition |
fieldidentifies the entity field by name (e.g.,"title","author"). For object/relationship fields you can also pass the full dotted path here as a shorthand (e.g.,"author.name","author.country.name")pathis required for object/relationship fields whenfieldis just the top-level name (e.g.,"name","country.name"). Omitpathif you used the dotted-fieldshorthand. Supplying both is an error.- Multiple
conditionsin the same group are combined with AND - Maximum nesting depth: 5 levels
Collection Field Filtering
Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.
Basic Collection Filtering
Filter collection fields using the same operators and format as main queries:
query {
series {
seasons(number: { operator: EQ, value: 1 }) {
number
id
year
}
}
}
Advanced Collection Filtering
You can use complex filtering with nested object properties:
query {
series {
seasons(
year: { operator: GTE, value: 2020 }
episodes: {
terms: [
{
path: "name",
operator: LIKE,
value: "Pilot"
}
]
}
) {
number
year
episodes {
name
date
}
}
}
}
Collection Filtering with Multiple Conditions
Combine multiple filter conditions for collection fields:
query {
series {
seasons(
number: { operator: GT, value: 1 }
year: { operator: BTW, value: [2015, 2023] }
) {
number
year
state
}
}
}
Nested Collection Filtering
Filter deeply nested collections using dot notation:
query {
series {
seasons(
episodes: {
terms: [
{
path: "name",
operator: LIKE,
value: "Final"
}
]
}
) {
number
episodes {
name
date
}
}
}
}
Collection Filtering with Array Operations
Use array operations for collection fields:
query {
series {
seasons(
categories: { operator: IN, value: ["Drama", "Crime"] }
) {
number
categories
}
}
}
Note: Collection field filtering uses the exact same format as main query filtering, ensuring consistency across your GraphQL API. All available operators (EQ, NE, GT, LT, GTE, LTE, LIKE, IN, NIN, BTW) work with collection fields.
🔗 Relationships
Defining Relationships
Use the extensions.relation field to define relationships between types:
const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
books: {
type: new GraphQLList(BookType),
extensions: {
relation: {
connectionField: 'author',
displayField: 'title'
},
},
// resolve method automatically generated! 🎉
},
}),
});
const BookType = new GraphQLObjectType({
name: 'Book',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
author: {
type: AuthorType,
extensions: {
relation: {
displayField: 'name'
},
},
// resolve method automatically generated! 🎉
},
}),
});
Relationship Configuration
connectionField: (Required for referenced collections) The child field linking back to the parent. For a single-object reference, this optionally overrides the stored ObjectId field; when omitted, model generation, writes, clears, and resolvers use the GraphQL field name.displayField: (Optional) Field to use for display in UI componentsembedded: (Optional) Whether the relation is embedded (default: false)
Auto-Generated Resolve Methods
🎉 NEW: Simfinity.js automatically generates resolve methods for relationship fields when types are connected, eliminating the need for manual resolver boilerplate.
Before (Manual Resolvers)
const BookType = new GraphQLObjectType({
name: 'Book',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
author: {
type: AuthorType,
extensions: {
relation: {
displayField: 'name'
},
},
// You had to manually write this
resolve(parent) {
return simfinity.getModel(AuthorType).findById(parent.author);
}
},
comments: {
type: new GraphQLList(CommentType),
extensions: {
relation: {
connectionField: 'bookId',
displayField: 'text'
},
},
// You had to manually write this too
resolve(parent) {
return simfinity.getModel(CommentType).find({ bookId: parent.id });
}
}
}),
});
After (Auto-Generated Resolvers)
const BookType = new GraphQLObjectType({
name: 'Book',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQL
Similar ai infra
n8n
Workflow automation platform for technical teams — visually build AI agent workflows with 400+ integrations
supabase
Postgres development platform — open-source alternative to Firebase with built-in AI/vector tools
AppFlowy
AI collaborative workspace — self-hosted Notion alternative for projects, wikis, and data control
coolify
Self-hostable PaaS alternative to Vercel, Heroku, Netlify — deploy static sites, databases, and full-stack apps on your own servers