npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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.

npm version npm downloads Docker Pulls Node.js TypeScript License: MIT

// 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:semantic

Prefer the lean edition without the embedding engine? Use the latest tag. Prebuilt packages (apt, macOS, Windows) at https://montygovernance.com.

Installation

npm install montycat

or with Yarn:

yarn add montycat

ESM-only, Node.js 18+. This package ships native ES modules — use import, not require(). 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-semantic edition; the default lean montycat server does not include it. Get it the way that suits you — pull the Docker image (montygovernance/montycat:semantic), download the prebuilt package, or install montycat-semantic from 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 the bge-small model).

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 exit

Tune it if you need to:

pool: { maxIdle: 4, idleTimeoutMs: 15000 }     // defaults: 8, 30000

Pools 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 -sha256
const 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. certificateVerification is 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
  • 📝 ChangelogCHANGELOG.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-semantic server. 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.