Skip to content

VSKI v0.10.0

Most tenants that needed graph workloads ran Neo4J alongside VSKI. Looking at actual usage, 99% of those use cases - agent graphs, knowledge linking, dependency walking - never touched the advanced features (Cypher, clustering, multi-hop pattern matching at scale) and never exceeded 100k edges. What they did incur was the cost of running and maintaining a separate database service: another deployment unit, another backup/replication story, another source of truth to keep consistent with VSKI data, another thing on-call has to know.

Light graphs move that workload into VSKI itself. Graphs are now a first-class entity type alongside collections - same (dbName, name) namespacing, same access-rule model, same single SQLite storage engine, same replication - so the graph and the content it indexes live in one place. For the workloads tenants actually run, this removes the Neo4J dependency and its engineering overhead entirely; no new infrastructure ships with it.

A graph is a named, db-scoped registry entry (same tombstone/revive semantics as collections) whose data lives in two physical tables in the tenant database: graph_<name>_nodes and graph_<name>_edges (labelled, directed edges with optional numeric weight, cascading deletes on endpoints). SQLite stays the source of truth - no separate service, nothing new to operate.

Traversal performance comes from an in-memory index: on first access each graph’s topology (ids, edge labels, weights - never properties) is loaded into a compact adjacency index with nodes and edges interned to uint32 slots. A fully-warm 100k-node / 100k-edge graph occupies ~20-25 MB and a worst-case BFS runs in single-digit milliseconds. Mutations serialize per graph (check + SQL write + index update under one lock), so size caps cannot be raced and the index cannot drift from committed state. Cold graphs are evicted LRU (GRAPH_INDEX_MAX_GRAPHS, default 32) and reload from SQLite on next touch; replicas revalidate against the database epoch and reload transparently.

Registry (mirrors /api/collections): GET/POST /api/graphs, GET/PATCH/DELETE /api/graphs/:name (soft delete keeps data; re-creating the same name revives the graph with its contents).

Data & traversal:

  • GET/POST /api/graphs/:name/nodes, GET/PATCH/DELETE .../nodes/:nodeId
  • GET/POST /api/graphs/:name/edges, GET/PATCH/DELETE .../edges/:edgeId
  • GET .../nodes/:nodeId/neighbors?direction=out|in|both&labels=a,b&limit=
  • GET .../nodes/:nodeId/traverse?depth=N&direction=&labels=&limit= (BFS, max depth 10)
  • GET .../path?from=&to=&maxHops=&cost=hops|weight&labels= (BFS or Dijkstra over edge weights)
  • GET .../subgraph?nodes=a,b&hops=N - neighborhood around seed nodes
  • GET .../stats - node/edge counts, label histogram, degree stats, effective caps

Nodes and edges accept single objects or arrays on create; ids are optional (15-char generated). Deleting a node cascades its edges.

The five collection rule fields apply with collection semantics: empty rule denies non-admins, "*" is public, admins bypass. List endpoints evaluate rules per item (non-trivial rules scan ahead to fill the page; the response carries "filtered": true and counts only visible items when that happened). Traversal endpoints are gated by viewRule.

GRAPH_MAX_EDGES / GRAPH_MAX_NODES (default 100000 each) bound every graph server-wide; a graph may declare a lower per-graph cap via options.maxEdges / options.maxNodes. Exceeding the cap is a clean 400 (graph edge cap exceeded (...)) from the per-graph write lock.

The caps exist to keep the memory story boring: every graph is bounded, so the total index footprint is bounded by the number of warm graphs, and nothing about the feature changes VSKI’s single-binary, single-file storage profile.

  • TypeScript/JS (@vski/sdk v2.1.0): new client.graph(name, db?) / client.db(name).graph(name) namespace (node/edge CRUD, neighbors, traverse, path, subgraph, stats) and client.settings.graphs registry namespace. New types: GraphConfig, GraphNode, GraphEdge, GraphStats, and the traversal result types.
  • Python (vski-sdk v0.4.0): matching surface - client.graph(name, db) / client.db(name).graph(name) (get_nodes/create_nodes/ get_node/update_node/delete_node, the same five for edges, neighbors, traverse, path, subgraph, stats) plus the client.settings.graphs registry namespace and GraphConfig/ GraphNode/GraphEdge/GraphStats TypedDicts. The sync wrapper mirrors both (SyncVskiClient.graph(...)). The e2e harness (including the replica suite) now honours VSKI_BINARY/VSKI_DIR overrides, so it runs against any server checkout; the suite passes on pytest 8 and 9.
  • Collections: the drop index/view/FTS routes (DELETE /api/collections/:name/indexes/:indexName, .../views/:viewName, .../fts/:ftsIndexName) were documented and implemented but never registered by the router - every call 404ed. They are wired now; the SDK drop_view/drop_index/drop_fts_index methods work against them.
  • Python SDK docs: set_views was documented as full-list replacement, but the server only ever adds views - slugs missing from the new list stay until dropped individually. The docstring now states the additive semantics and points at drop_view (which takes the full materialised <collection>_view_<slug> name).
  • No breaking changes. The graphs registry table and the unique index uq_graphs_db_name are created idempotently in default.db on boot; tenant tables are created per graph on demand.
  • Existing Neo4J deployments are unaffected; migration to light graphs is per-workload (create the graph, replay nodes/edges through the SDK or a workflow) and can be done incrementally.
  • Graph mutations publish realtime events on the db:<graph> topic (subscription UI support follows in the dashboard update).
  • Dashboard: graph management pages are not part of this release; use the SDK or the SQL console until the follow-up web update.