VSKI v0.10.0
Light Graphs: retiring the Neo4J sidecar
Section titled “Light Graphs: retiring the Neo4J sidecar”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.
How it works
Section titled “How it works”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/:nodeIdGET/POST /api/graphs/:name/edges,GET/PATCH/DELETE .../edges/:edgeIdGET .../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 nodesGET .../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/sdkv2.1.0): newclient.graph(name, db?)/client.db(name).graph(name)namespace (node/edge CRUD,neighbors,traverse,path,subgraph,stats) andclient.settings.graphsregistry namespace. New types:GraphConfig,GraphNode,GraphEdge,GraphStats, and the traversal result types. - Python (
vski-sdkv0.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 theclient.settings.graphsregistry namespace andGraphConfig/GraphNode/GraphEdge/GraphStatsTypedDicts. The sync wrapper mirrors both (SyncVskiClient.graph(...)). The e2e harness (including the replica suite) now honoursVSKI_BINARY/VSKI_DIRoverrides, 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 SDKdrop_view/drop_index/drop_fts_indexmethods work against them. - Python SDK docs:
set_viewswas 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 atdrop_view(which takes the full materialised<collection>_view_<slug>name).
Compatibility
Section titled “Compatibility”- No breaking changes. The
graphsregistry table and the unique indexuq_graphs_db_nameare created idempotently indefault.dbon 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.