montycat
v1.3.4
Published
Self-hosted vector database + NoSQL with built-in AI semantic search — the Node.js & TypeScript client for Montycat. A Rust-powered, AI-native Pinecone / Weaviate / Chroma alternative for RAG, AI agents & LLM memory.
Maintainers
Keywords
Readme
🚀 Montycat for Node.js & TypeScript — The AI-Native NoSQL Database with Semantic Search for RAG & Agents
Abolish the two-database stack.
The official Node.js & TypeScript SDK for Montycat — a self-hosted NoSQL + vector database with AI semantic search forged into the core, built for RAG and AI-agent memory. One Rust engine, not a sprawl of services. Your hardware. Your data. Your meaning.
// Search your data by MEANING — no external APIs, no separate vector database.
// (already ON by default in the montycat-semantic server edition)
const hits = await Sales.searchValues({ query: 'Show all Bluetooth devices', limitOutput: { start: 0, stop: 5 } });
// → [{ __key__: 123..., __score__: 0.78, __value__: { name: 'Wireless Headphones' }}]🧩 All-in-one. AI-native. Zero external dependencies.
The vector-embedding engine runs inside the database — no separate vector DB, no embedding API, no API keys, no sidecar service. One engine, one binary, your hardware.
What is Montycat?
For a generation we were told the price of intelligence was two systems: a database for your records, and a separate vector store — with its per-query bill — for their meaning. Montycat rejects that tax. It is a self-hosted NoSQL + vector database: one Rust-powered engine with semantic search built in, so RAG, AI-agent memory, and vector search live where your data already lives. No cloud lock-in. No ops headache. Decentralized by nature, ultra-fast, and natively async.
Think of it as an open-source, self-hosted alternative to Pinecone, Weaviate, Chroma, Qdrant, and Redis — a vector database and a NoSQL store in a single engine, so your records and their embeddings live together instead of in two systems you have to keep in sync.
This client gives JavaScript and TypeScript developers structured, decentralized, secure data through a clean async API that feels native to the language — not another bloated driver. Montycat is not an incremental improvement on the databases you know. It is a break with them.
🧭 The Montycat Philosophy
- 💡 No query language. No SQL. No CQL. No pseudo-ORM DSLs. Just structured, composable function calls.
- ⚡ Async-first by design. Every operation is fully non-blocking, built for Node.js concurrency and scale.
- 🔐 Safe by construction. No injections, no loose queries, no ambiguity — only typed, structured data access.
- 🧩 Data Mesh native. Every keyspace is its own domain — decentralized, independent, and composable.
- 🧠 Developer-centric. Clean APIs, minimal setup, and full schema-awareness — because data shouldn’t be painful.
Montycat isn’t just another database with a JavaScript client. It’s a Rust-powered data engine that speaks JavaScript fluently.
✨ Key Features
- ⚡
High Performance- Ultra-fast read/write operations powered by Rust’s zero-copy architecture. - 💾
In-Memory + Persistent- Combine memory-speed operations with durable persistence — seamlessly. - 🔄
Async/Await Native- Built for modern async I/O — perfect for APIs, microservices, and real-time apps. - 🧭
Data Mesh Ready- Decentralized keyspaces for distributed domain ownership. - 📡
Real-Time Subscriptions- Subscribe to keyspace or key changes with live updates. - 🔐
TLS Security- Encrypted communication and authenticated connections. - 🧬
Schema Support- Optional schema layer for validation, structure, and type safety. - 🧠
AI Semantic & Vector Search- Rank stored items by meaning with on-device embeddings. kNN vector search for RAG, AI agents & LLM apps — no external API, no separate vector database. - 🧱
Zero Dependencies on ORMs- No extra abstractions. Just pure, beautiful logic. - 🧠
Easy Integration- Works with Express, Fastify, Next.js, Deno, and any Node.js runtime.
🔐 Security & Reliability
- End-to-end TLS support for encrypted transport
- Safe async concurrency — no race conditions or data leaks
- Domain-level data isolation
🧠 Why Developers Love Montycat
- 🧱 One protocol. One mindset. No complex dialects or query languages — just structured logic.
- ⚙️ Plays nicely with modern stacks. Works out-of-the-box with Express, Koa, Fastify, Deno, Bun, and Electron.
- 💬 Reactive by nature. Subscriptions make Montycat ideal for dashboards, analytics, and live apps.
- 🪶 Minimal footprint. Lightweight Node.js client backed by a Rust core — zero bloat.
🏁 The Future Is Structured
Other databases were written in C, C++, or Java — for a world that no longer exists.
Montycat was built in Rust and made to speak JavaScript and TypeScript fluently.
No adapters. No legacy baggage. No permission asked. Just pure async data flow.
The old stack — a database here, a vector store there, an embedding API somewhere in the cloud, and a tangle of sync jobs holding it together — is not an architecture. It is a compromise. Montycat refuses the compromise: one engine, your hardware, your data, your meaning.
When you use Montycat, you are not querying a database. You are interacting with a living data mesh that speaks your language.
🔍 Example Use Cases
- RAG pipelines & semantic retrieval for LLM-powered apps
- AI agent / chatbot long-term memory that survives restarts
- Semantic product search & recommendations — match intent, not keywords
- Real-time dashboards, analytics, and live collaborative apps
- Microservice data stores and event-driven systems
- Data products in a decentralized Mesh architecture
🚀 Get the Engine (30 seconds)
The client talks to a Montycat server. Fastest way — Docker, with AI semantic search built in:
docker run -d --name montycat \
-p 21210:21210 -p 21211:21211 \
-e MONTYCAT_SUPEROWNER="admin" \
-e MONTYCAT_PASSWORD="change-me" \
-v montycat_data:/var/lib/.montycat \
montygovernance/montycat:semanticPrefer the lean edition without the embedding engine? Use the latest tag. Prebuilt packages (apt, macOS, Windows) at https://montygovernance.com.
Installation
npm install montycator with Yarn:
yarn add montycatESM-only, Node.js 18+. This package ships native ES modules — use
import, notrequire(). Works out of the box with TypeScript, Deno, Bun, and any modern Node.js project ("type": "module"or.mjs).
Quick Start
TypeScript Example
import {Engine, Keyspace, Pointer, Schema, Timestamp} from 'montycat'
// setup connection
interface EngineConfig {
store: string;
port: number;
username: string;
password: string;
host: string;
}
const engineConfig: EngineConfig = {
store: 'Company',
port: 21210,
username: 'user',
password: 'password',
host: '127.0.0.1',
};
const engine: Engine = new Engine(engineConfig);
// define data structures
class Sales extends Keyspace.Persistent {
static keyspace = "Sales";
}
class Production extends Keyspace.InMemory {
static keyspace = "Production";
}
Sales.connectEngine(engine);
Production.connectEngine(engine);
// create keyspaces and store
const res1 = await Sales.createKeyspace();
const res2 = await Production.createKeyspace();
console.log('Keyspace creation results:', res1, res2);
// define data schemas (optional)
interface SalesSchemaInterface {
item: string;
amount: number;
};
class SalesSchema extends Schema {
constructor({ item, amount }: SalesSchemaInterface) {
super({ item, amount });
}
};
interface ProductionSchemaInterface {
workOrder: string;
customer: string;
};
class ProductionSchema extends Schema {
constructor({ workOrder, customer }: ProductionSchemaInterface) {
super({ workOrder, customer });
}
}
// insert values
const newSale = new SalesSchema({
item: 'Product 1',
amount: 10,
}).serialize();
const newOrder = new ProductionSchema({
workOrder: 'WO 000012',
customer: 'ACME Corp',
}).serialize();
const res3 = await Sales.insertValue({ value: newSale });
const res4 = await Production.insertValue({ value: newOrder });
console.log('Insertion results:', res3, res4);
// check insertions
const res5 = await Sales.lookupValuesWhere({
searchCriteria: { item: 'Product 1' },
keyIncluded: true
});
const res6 = await Production.lookupValuesWhere({
searchCriteria: { customer: 'ACME Corp' },
schema: ProductionSchema,
keyIncluded: true
});
console.log('Lookup results:', res5, res6);
JavaScript Example
import {Engine, Keyspace, Pointer, Schema, Timestamp} from 'montycat'
// setup connection
const engineConfig = {
store: 'Company',
port: 21210,
username: 'user',
password: 'password',
host: '127.0.0.1',
};
const engine = new Engine(engineConfig);
// define data structures
class Sales extends Keyspace.Persistent {
static keyspace = "Sales";
}
class Production extends Keyspace.InMemory {
static keyspace = "Production";
}
Sales.connectEngine(engine);
Production.connectEngine(engine);
// create keyspaces and store
const res1 = await Sales.createKeyspace();
const res2 = await Production.createKeyspace();
console.log('Keyspace creation results:', res1, res2);
// define data schemas (optional)
class SalesSchema extends Schema {
constructor({ item, amount }) {
super({ item, amount });
}
};
class ProductionSchema extends Schema {
constructor({ workOrder, customer }) {
super({ workOrder, customer });
}
}
// insert values
const newSale = new SalesSchema({
item: 'Product 1',
amount: 10,
}).serialize();
const newOrder = new ProductionSchema({
workOrder: 'WO 000012',
customer: 'ACME Corp',
}).serialize();
const res3 = await Sales.insertValue({ value: newSale });
const res4 = await Production.insertValue({ value: newOrder });
console.log('Insertion results:', res3, res4);
// check insertions
const res5 = await Sales.lookupValuesWhere({
searchCriteria: { item: 'Product 1' },
keyIncluded: true
});
const res6 = await Production.lookupValuesWhere({
searchCriteria: { customer: 'ACME Corp' },
schema: ProductionSchema,
keyIncluded: true
});
console.log('Lookup results:', res5, res6);
🧠 Ranked Search — Semantic, BM25 Keyword, and Hybrid
Montycat provides semantic vector search, persistent BM25 keyword search, and
hybrid ranking in one database. Use lookup* for exact structured matching;
use searchKeys or searchValues for relevance-ranked retrieval.
- 🔎 Semantic / vector search — kNN similarity over on-device embeddings, not brittle keyword matches.
- 🤖 Built for AI — RAG, semantic retrieval, AI agents, recommendations, dedup, clustering.
- 🔒 Private & free — embeddings never leave your machine. No OpenAI/Cohere bill, no data egress.
- ⚡ One system, not two — your data and its vectors live in the same database. No sync jobs, no drift, no second service to run.
- 🚀 Zero setup — no index tuning, no pipeline:
enableSemanticSearch()and you're ranking by meaning.
⚠️ Requires the semantic edition of the server — nothing to compile. Semantic search runs an embedded ONNX vector-embedding engine that ships only in the
montycat-semanticedition; the default leanmontycatserver does not include it. Get it the way that suits you — pull the Docker image (montygovernance/montycat:semantic), download the prebuilt package, or installmontycat-semanticfrom the apt repository. The Node.js client API is identical either way; just point it at a semantic-edition server (semantic search is enabled by default there, using thebge-smallmodel).
The switch is DB-wide and already on in the semantic edition; every keyspace is embedded in the background as data is written (the embedding model is downloaded on demand).
import { SearchMode } from 'montycat';
// Semantic search is ON by default in the montycat-semantic edition — just search.
// Rank stored items by meaning — two flavors:
// getValues → each hit is { __key__, __score__, __value__ }
// getKeys → each hit is { __key__, __score__ } (lighter; fetch a page later with getBulk)
const hits = await Sales.searchValues({
query: 'Show all Bluetooth devices',
mode: SearchMode.Hybrid,
limitOutput: { start: 0, stop: 5 },
});
const keys = await Sales.searchKeys({
query: 'bluetooth',
mode: SearchMode.Keyword,
limitOutput: { start: 0, stop: 5 },
});
// Optionally drop weak matches by cosine similarity (range [-1, 1]).
const strong = await Sales.searchKeys({ query: 'Show all Bluetooth devices', mode: SearchMode.Semantic, limitOutput: { start: 0, stop: 5 }, minScore: 0.35 });
// Control the DB-wide switch (optional — it's already on):
// Read back the model and backfill state actually assigned to a keyspace.
const status = await engine.getSemanticStatus({
store: 'catalog',
keyspace: 'products',
});
// After globally re-enabling semantic search, retry searches while
// status.payload.reloading is true: retained indexes open in the background.
// status.payload.indexing reports live and backfill queue depths.
// Enable an unenrolled keyspace with an explicit model.
await engine.enableSemanticSearch({
model: SemanticModel.BGE_BASE,
store: 'catalog',
keyspace: 'products',
});
// Changing an enrolled keyspace is destructive and starts a full backfill.
await engine.reembedSemanticSearch({
model: SemanticModel.BGE_BASE,
store: 'catalog',
keyspace: 'products',
});
// turn it off (vectors are kept so re-enabling resumes instantly;
// pass { dropVectors: true } to also clear stored vectors)
await engine.disableSemanticSearch();Search modes and metadata filters
Semantic ranks by vector similarity, Keyword uses BM25, and Hybrid
combines both ranked lists with reciprocal-rank fusion. Optional filters are
an exact hard pre-filter and do not affect relevance scores.
__score__ is cosine similarity in semantic mode, raw BM25 relevance in
keyword mode, and a normalized [0, 1] RRF score in hybrid mode. Keyword
scores have no fixed upper bound, so compare scores only within the same query
and search mode. A hybrid score near 1.0 means strong agreement between both
rankings; a top result found by only one branch is around 0.5. minScore
filters the final selected mode score before pagination. In hybrid mode this
means the fused RRF score; keyword-only fallback hits are filtered too.
const matchingKeys = await Sales.searchKeys({
query: 'astronomy and outer space',
mode: SearchMode.Hybrid,
filters: { category: 'space' },
limitOutput: { start: 0, stop: 5 },
minScore: 0.35,
});
const matchingValues = await Sales.searchValues({
query: 'astronomy and outer space',
mode: SearchMode.Hybrid,
filters: { category: 'space' },
limitOutput: { start: 0, stop: 5 },
});
// key hits: { __key__, __score__ }
// value hits: { __key__, __score__, __value__ }Bring your own vectors
If you already have embeddings from a batch pipeline or vector store, first enroll the keyspace for externally generated vectors. External profiles support 1–4,096 dimensions for OpenAI-style 1,536d pipelines, Pinecone/Qdrant/Milvus migrations, and image or multimodal vectors:
await Items.createKeyspace({ semantic: false });
await engine.enablePrecomputedVectorSearch({
store: 'app', keyspace: 'items', dimensions: 1536,
embeddingSpace: 'text-embedding-3-small:v1',
});embeddingSpace is a descriptive name for the vectors' model/configuration;
it does not invoke or validate that model. Then supply vectors directly and the
server skips embedding.
Needs a Montycat Semantic server 1.3.0 or newer.
// Writing: pass `vector` alongside the value.
await Sales.insertValue({
value: { text: 'The Voyager probes left the heliosphere.' },
vector: myEmbedding, // number[]
});
// Bulk: paired with `bulk` by position.
await Sales.insertBulk({
bulk: [doc1, doc2],
vectors: [embedding1, embedding2],
});
// Searching: pass a query vector; the query string may be empty.
const hits = await Sales.searchValues({
query: '',
mode: SearchMode.Semantic,
vector: myQueryEmbedding,
limitOutput: { start: 0, stop: 10 },
});vector is also accepted by insertCustomKeyValue and updateValue, and
updateBulk takes vectors for numeric keys plus customVectors for custom
keys. searchKeys and searchValues accept a query vector in semantic mode.
Serialized Schema values can be passed directly to updateBulk. Their
schema property is transported as request metadata rather than stored as a
document field, while nested timestamps metadata remains intact. Every value
in one bulk update must use the same schema.
Embedding-space compatibility is required. Every supplied record vector and query vector must be produced by the model enrolled for that keyspace, including the same model revision, preprocessing, pooling, and normalization. Matching the dimension alone is not enough: an auto-enrolled BGE-small keyspace accepts only BGE-small-compatible 384d vectors. To use vectors from another model, create the keyspace with semantic auto-enrollment disabled and enroll a matching external profile first. The server validates dimensions before anything reaches the index, but it cannot prove that two equal-length vectors came from the same embedding space. A vector you supplied will not be overwritten by background embedding; a later ordinary write to that item clears the protection and re-embeds from its text, which is when re-embedding is what you want.
Mixing is fine: items with supplied vectors and items the server embeds can live in one keyspace as long as every vector comes from the same model.
📨 Response Shape
Every call resolves to the same envelope, so there is one thing to check everywhere:
// { status: true, payload: <result>, error: null }
// { status: false, payload: null, error: "Governance permission denied: ..." }
const res = await Sales.insertValue({ value: newSale });
if (res.status) console.log(res.payload);payload is null for commands that only acknowledge, the new key for inserts, and an
array for lookups and semantic searches. Keys are u128 and always arrive as strings —
never pass one through Number(), which silently loses precision above 2^53. Invalid
arguments throw before anything touches the network; server-side failures come back in
error with status: false.
🔄 Connection Pooling
By default every request opens a TCP connection, sends, reads one response, and closes. Reuse the connection instead and the handshake disappears from every call after the first. The win scales with how much of your latency is connection setup: large for a chatty service issuing many small reads, larger over a network — where the handshake costs a full round trip before the query is even sent — and larger again with TLS.
Pooling is opt-in. One new field, and no call site changes:
import { Engine, closeAllPools } from 'montycat';
const engine = new Engine({
host: '127.0.0.1', port: 21210, username: 'user', password: 'password',
store: 'Company',
pool: {}, // ← the only new field
});
Sales.connectEngine(engine);
await Sales.insertValue({ value: newSale }); // unchanged
closeAllPools(); // before exitTune it if you need to:
pool: { maxIdle: 4, idleTimeoutMs: 15000 } // defaults: 8, 30000Pools are shared per endpoint and TLS trust configuration. They live in a module-level registry,
not on the Engine, so two keyspace classes pointing at the same server share one pool
rather than each opening its own. The complete TLS configuration is part of the key, so
plaintext, TLS, and connections using different certificate pins are never interchangeable.
Keep maxIdle modest. An idle pooled connection still holds one of the engine's
connection permits. The defaults are deliberately small; raise them only after measuring
with queueDepths() under realistic load.
Call closeAllPools() before exit, otherwise idle sockets keep the process alive.
Subscriptions are never pooled — they are long-lived, stream many responses to one request, and live on their own port. A connection is held exclusively for one request/response, so concurrent calls each get their own rather than interleaving writes on one socket.
📡 Real-Time Subscriptions
Subscribe to one key or to a whole keyspace and get pushed every change — the reactive core behind live dashboards, analytics, and event-driven services.
// Whole keyspace: omit both key and customKey.
const handle = await Sales.subscribe({
callback: (event) => console.log('changed:', event),
});
// Or watch a single key (customKey is hashed with XXH32 for you).
// Passing key and customKey together throws.
const oneKey = await Sales.subscribe({
key: '30442970696809394303186116932586352271',
callback: (event) => console.log('changed:', event),
});
// Stop listening and close the socket. No callback fires after this.
handle.stop();
oneKey.stop();subscribe resolves to a handle exposing stop(). Subscriptions use the subscription
port, port + 1 — that is the second port (21211) published in the Docker command
above.
🔐 TLS
Set useTls on the engine config to negotiate an encrypted connection. It applies to
commands and subscriptions alike:
const engine = new Engine({
store: 'Company',
port: 21210,
username: 'user',
password: 'password',
host: '127.0.0.1',
useTls: true,
});On its own that encrypts the connection without checking who is on the other end, which is where this client has always stood. Encryption without verification stops passive eavesdropping but not an active attacker: anything that can sit in the path can present its own certificate and read or alter every request, credentials included.
Verifying the engine
Verification is opt-in, and takes whichever form of trust material you have.
The engine's certificate, copied to the client host. The certificate the engine presents must match this file exactly:
const engine = new Engine({
// ...
useTls: true,
certificatePath: '/etc/montycat/server.crt',
});Its SHA-256 fingerprint, when passing a string is easier than shipping a file — a container image, an environment variable, a secrets manager:
openssl x509 -in server.crt -noout -fingerprint -sha256const engine = new Engine({
// ...
useTls: true,
certificateFingerprint: process.env.MONTYCAT_CERT_FINGERPRINT,
});Either one implies verification — no second option needed. Both pin the same leaf
certificate identity: a certificate file compares parsed DER bytes, while a fingerprint
compares its SHA-256 digest. Pinning skips hostname checking because the engine's
self-signed certificate carries only localhost, 127.0.0.1 and ::1 as subject
alternative names unless it was regenerated with init-self-tls dns/ip. The
comparison already answers the question a hostname check is a proxy for.
A certificate from a real CA, for an engine behind a terminating proxy — no pin,
so rejectUnauthorized applies with ordinary hostname checking:
const engine = new Engine({ /* ... */ useTls: true, certificateVerification: true });A certificate that does not match fails before any request byte is written, on the pooled and per-request paths alike. Following this client's convention the failure is returned as an error string rather than thrown, the same way every other connection error is here — and it names the fingerprint that actually arrived, so a regenerated certificate is a one-line fix.
Note.
certificateVerificationis off by default. Turning it on by default would break every deployment using the engine's self-signed certificate, so the choice is yours to make explicitly.
👥 Owners & Access
Governance policies below are written against owners, so create them first. A superowner provisions an owner, then grants data access — optionally narrowed to specific keyspaces:
import { ValidPermissions } from 'montycat';
await engine.createOwner({ owner: 'alice', password: 'alice-password' });
await engine.grantTo({ owner: 'alice', permission: ValidPermissions.READ });
await engine.grantTo({
owner: 'alice',
permission: ValidPermissions.WRITE,
keyspaces: ['Sales'],
});
await engine.listOwners();
await engine.revokeFrom({
owner: 'alice',
permission: ValidPermissions.WRITE,
keyspaces: ['Sales'],
});
await engine.removeOwner({ owner: 'alice' });ValidPermissions is READ, WRITE, or ALL; the tokens are normalized and an
unknown one throws. grantTo and revokeFrom apply to the engine's store. This
governs data access; to delegate administrative capabilities such as provisioning
keyspaces or managing schemas, see
Data-mesh governance at
the end of this document.
🔗 Links
- 🌐 Website & Docs — https://montygovernance.com
- 📦 npm — https://www.npmjs.com/package/montycat
- 🐳 Docker Hub — https://hub.docker.com/r/montygovernance/montycat
- 💻 Source — https://github.com/MontyGovernance/montycat_node
- 📝 Changelog — CHANGELOG.md
❓ FAQ
- Is Montycat a vector database or a NoSQL database? Both — one engine. Store records and query them by meaning (vector / semantic search) or by key/schema, without running two systems.
- Do I need OpenAI or an embedding API? No. Embeddings run on-device in the
montycat-semanticserver. No API keys, no per-query bill, no data egress. - Is it a Pinecone / Weaviate / Chroma / Qdrant alternative? Yes — self-hosted and open-source, with a NoSQL store built in.
- TypeScript support? First-class — the package ships its own type definitions. Works with Node.js, Deno, Bun, Express, Fastify, and Next.js.
Data-mesh governance for shared and multi-tenant deployments
Delegate administration without giving every team full server control. Policies scope authority to an owner and store, with optional keyspace, storage-type, and semantic-model constraints. Platform teams can govern shared infrastructure while domain teams operate the data products they own.
- Grant, revoke, or explicitly deny keyspace provisioning/removal, schema, semantic, snapshot, and access-management capabilities.
- Inspect effective permissions and policy history, or preview a grant/revoke before applying it.
- Validate, plan, apply, and export JSON or YAML policy manifests for repeatable infrastructure-as-code workflows.
- Constrain storage types for provisioning, removal, schema, access, and semantic management. Snapshot management is always in-memory, so it takes no storage-type qualifier.
- Constrain semantic models during keyspace provisioning and semantic management.
For example, a superowner can restrict what Alice may provision and separately delegate semantic management for one keyspace:
import { PolicyCapability, PolicyKeyspaceType, SemanticModel } from 'montycat';
await engine.policyGrant({
owner: 'alice', capability: PolicyCapability.PROVISION_KEYSPACE, store: 'catalog',
types: [PolicyKeyspaceType.IN_MEMORY, PolicyKeyspaceType.PERSISTENT],
models: [SemanticModel.BGE_SMALL],
});
await engine.policyGrant({
owner: 'alice', capability: PolicyCapability.MANAGE_SEMANTIC, store: 'catalog',
keyspace: 'products',
types: [PolicyKeyspaceType.IN_MEMORY],
models: [SemanticModel.BGE_SMALL],
});
await engine.policyView({ owner: 'alice', store: 'catalog' });Use policyExplain to inspect an authorization decision and policyHistory to audit
changes. Superowners can manage policies directly with policyGrant, policyRevoke,
policyDeny, and policyRemoveDenial, or use policyValidate, policyPlan,
policyApply, and policyExport with JSON or YAML documents.
