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 framework repository; the link is coming soon. 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.