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',
},
},
}); 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.
Pre-1.0. APIs change without notice. Do not use in production.
const Note = type('Note', {
schemaId: '@notes',
fields: {
title: field.string({ required: true }),
content: field.string({ required: true }),
tags: field.string({ multiValued: true }),
},
});
// the type definition above is itself a node
// in the same graph as your data. 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.
- 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.
- 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.
- 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.
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.
await Note.query({
title: { contains: 'Graph' },
$orderBy: { createdAt: 'desc' },
$limit: 20,
}).exec(); 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; 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; 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 · libSQLSuggested 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 graphWhen 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 persistenceTests, 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 indexPortable, 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.
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.
{
enforceRequiredFields: false,
allowBorrowingFields: true,
noShadowing: false,
} 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.
{
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)
Where to go next
Read the design. Read the code.
Everything below lives in the same GitHub repository. Documentation is honest about which parts are settled and which are still moving — the Overview is the best single entry point if you want the comprehensive picture.