Stellium · v0.0.0 Alpha

The schema lives
in the data.

Stellium is a TypeScript framework where type definitions, field definitions, and the composition edges between them are nodes in the same graph as your data. Schema migrations become a reconcile operation. LLM agents discover the data model by querying it.

One codebase runs against SQL (SQLite, Postgres, libSQL), Neo4j, in-memory JSON, or Markdown over the filesystem — same DSL, backend-appropriate emission.

Pre-1.0. APIs change without notice. Do not use in production.

Core commitment

One graph for schema and data.

TypeDefinition, FieldDefinition, and the COMPOSES edges between them are nodes — queryable with the same primitives as the records they describe. Most frameworks keep the schema in code and the data in a database. Stellium keeps both in the graph.

  1. 01

    Schema migrations are diffs.

    Compare your declared types against the live graph, classify each change as additive, behavioral, or destructive, and apply in dependency order inside a transaction. No hand-written migration scripts; the engine reasons over the same data the application reads.

  2. 02

    The system describes itself.

    An LLM agent queries TypeDefinition the same way it queries your data. The schema is discoverable by walking the graph — there is no parallel metadata channel and no documentation that can silently drift out of sync.

  3. 03

    Export carries the schema.

    Because the schema is data, exporting the data exports the schema with it. Sovereignty is structural: data ports out with its type definitions attached, which is what makes "leaving" actually work rather than rhetorically work.

The shape of the work

Declare a type. Generate the class. Query it.

The DSL on the left registers a type with Stellium. Running the code generator produces a typed domain class with CRUD, relationship accessors, and a query builder — the snippet on the right uses it. The query compiler emits Cypher for Neo4j, SQL for the SQL backend, and native predicate evaluation for JSON and Markdown — same input shape, backend-appropriate output.

schema.ts Define the type
import { type, field, relationship } from '@stellium/schema';

const TAGGED_WITH = relationship('TAGGED_WITH', {
  schemaId: '@notes',
});

const Note = type('Note', {
  schemaId: '@notes',
  fields: {
    title:   field.string({ required: true }),
    content: field.string({ required: true }),
    tags:    field.string({ multiValued: true }),
  },
  relationships: {
    topics: {
      ref: TAGGED_WITH,
      target: 'Topic',
      direction: 'outgoing',
      cardinality: 'many',
    },
  },
});
app.ts Use the generated class
import { Note } from './generated/domain.js';

// Create
const note = await Note.create({
  title: 'Graph databases',
  content: 'Property graphs store key-value pairs...',
  tags: ['databases', 'graphs'],
});

// Type-safe query builder
const recent = await Note.query({
  title: { contains: 'Graph' },
  $orderBy: { createdAt: 'desc' },
  $limit: 20,
}).exec();

// Relationship traversal
const topics = await note.getTopics();

Intermediate representation

A backend-agnostic IR, four backend-specific emitters.

A query you write is a plain TypeScript object: shape predicates, ordering, projections, traversal. On every terminal call, the classifier partitions it into a QueryIR — a backend-agnostic plan whose only backend-aware input is the type's resolved storage mode. The active engine reads that IR and emits its native form: Cypher for Neo4j, parameterized SQL against real typed columns for SQLite, Postgres, and libSQL, or a direct predicate walk for the in-memory JSON store and the Markdown sidecar index. No lowest-common-denominator; each emission is what the backend is good at.

Stellium query compiler pipeline A TypeScript query object is classified, lowered to a backend-agnostic intermediate representation called QueryIR, then compiled and stitched into either Cypher (Neo4j), SQL (SQL engine), or a native predicate walk (JSON or Markdown backends). QUERY OBJECT Note.query({...}) classify QUERYIR Backend-agnostic tree emit per engine CYPHER Neo4j MATCH (n)-[:HAS_TYPE]->... SQL SQLite · Postgres · libSQL SELECT n.* FROM node n WHERE... PREDICATE WALK In-memory JSON · Markdown nodes.filter(...).sort(...)
Per terminal call: classify into IR, then engine-specific emit. No plan cache.
app.ts One query — the same input across all backends
await Note.query({
  title: { contains: 'Graph' },
  $orderBy: { createdAt: 'desc' },
  $limit: 20,
}).exec();
Cypher Neo4j
MATCH (n:Node)
  -[:HAS_TYPE]->
  (:TypeDefinition {name: 'Note'})
WHERE n.schemaId = '@notes'
  AND n.title CONTAINS 'Graph'
RETURN n
ORDER BY n.createdAt DESC
LIMIT 20;
SQL SQLite · Postgres · libSQL
SELECT n.*
FROM node n
WHERE n.schema_id = '@notes'
  AND n.type = 'Note'
  AND n.title LIKE '%Graph%'
ORDER BY n.created_at DESC
LIMIT 20;
Predicate walk JSON · Markdown
nodes
  .filter(n =>
    n.schemaId === '@notes'
    && n.types.has('Note')
    && n.title.includes('Graph'))
  .sort((a, b) =>
    b.createdAt - a.createdAt)
  .slice(0, 20);

Emissions above are illustrative. Real output depends on the active storage mode, indexes, and the query's classification; the SQL backend uses real typed columns and idiomatic predicates, not JSONB. The Markdown engine intentionally rejects a few IR features (raw fragments, relationship projections) and throws a typed BackendCapabilityError when they appear.

Backend profiles

Four engines. Different workloads, same surface.

With the IR in place, backend choice becomes a deployment decision rather than an architectural one. Each engine has a profile — operational footprint, query-shape strengths, persistence model — so pick whichever fits the workload. Backend selection is GraphConfig.type at initialization, and a native-query escape hatch is available for the rare case the IR doesn't cover.

  • SQL

    SQLite · Postgres · libSQL

    Suggested default

    Real typed columns with B-tree indexes, not JSONB. Familiar operational surface for any team that already runs Postgres. The query compiler emits standard SQL — you can EXPLAIN it.

  • Neo4j

    Cypher · property graph

    When the graph is the query pattern

    Native graph storage and traversal. Combined relationship + field queries measured at 6.6× faster than property-store mode at 100K nodes. The right choice when multi-hop reasoning is the workload.

  • JSON

    In-memory · optional file persistence

    Tests, demos, scratch graphs

    Fast and ephemeral. Same query surface and same DSL as the production backends. Useful for tests that need a real graph and for early-stage projects that have not yet committed to a database.

  • Markdown

    Filesystem · sidecar index

    Portable, git-trackable, hand-editable

    A graph backed by a folder of markdown files. Editable in any text editor while the framework continues to read it. Portable across machines without a migration step — the data is files.

Same DSL, same query builder, same domain classes — the engine selection is one line of config.

The strictness dial

Loose to strict by configuration.

The same codebase operates as a forgiving knowledge graph at one setting and as a strict ORM at the other. The transition is a configuration change, not a migration — the data does not move, only what the system accepts on the way in.

Forgiving

Loose mode

  • Missing required fields surface as warnings.
  • Nodes can borrow fields from types they do not carry.
  • Composition conflicts are tolerated.

Optimized for exploration and early-stage data. The schema is a guide, not a contract.

SchemaSettings
{
  enforceRequiredFields: false,
  allowBorrowingFields:  true,
  noShadowing:           false,
}
Enforced

Strict mode

  • Required fields enforced at write time.
  • Field borrowing rejected.
  • Composition conflicts caught at schema registration.

Optimized for machine reliability and production APIs. The schema is a contract.

SchemaSettings
{
  enforceRequiredFields: true,
  allowBorrowingFields:  false,
  noShadowing:           true,
}

Per-schema and per-type. Information can start loose and become strict as it matures — without changing tools.

Honest status

What's built, what's designed, what's not yet.

Stellium is a personal infrastructure project under active development. The split below tracks what exists in src/ today, what has settled architecturally but is only partially wired, and what lives only in design documents so far.

Built and operational

  • Schema DSL and code generation
  • Type composition (multi-type nodes, field borrowing, provenance)
  • Query compiler (classify → compile → stitch)
  • Reconciliation engine (diff → classify → cascade → apply)
  • Four pluggable backends (SQL, Neo4j, JSON, Markdown)
  • Dual storage modes (graph + direct) behind a uniform contract
  • Permission engine (RBAC + ABAC, circle-based)
  • Plugin system with capability gating
  • MCP server with progressive tool disclosure
  • DID-based auth, sessions, handles, rate limiting
  • CLI: init, generate, diff, apply, status
  • Data Coordinator (reactive provider/consumer wiring)

Direction settled, partial

  • Mutation pipeline (VECTOR Spine and Vertebrae)
  • Hook umbrella and Subscriber model
  • Three-layer auth decomposition
  • Configurable strictness extensions

Designed, not yet built

  • Presentation layer (intent-driven rendering, 30+ design docs)
  • Federation between Stellium instances (892-line spec)
  • Multi-instance isolation deployment
  • Real-time collaboration (CRDTs)
  • Reactive transformation chains (Currents)