---
title: Seele schema
scope: 3dmk
type: schema
subject: seele
status: current
updated: "2026-10-31"
source: https://3dmk.com/documentation#seele-schema
author: Nick Olson
contact:
  email: 3dmk@3dmk.com
  site: https://3dmk.com
license: CC BY 4.0
license_url: https://creativecommons.org/licenses/by/4.0/
attribution: Nick Olson, 3dmk.com
tags:
  - seele
  - postgresql
  - graph
  - documentation
---

# Seele schema

Seele stores documentation as a graph in PostgreSQL. Three tables hold it: the nodes, the edges between them, and a version history of every write. This file describes a new Seele installation with all three tables empty.

## What Seele holds

- **Node:** one unit of documentation. It carries an identity, a title, classification fields, Markdown content, and an optional embedding vector. Each node also records its token count and its created and modified dates.
- **Edge:** a named relation between two nodes. The relation sets how a read loads the target node. An optional rank orders the children of one parent.
- **Version:** a full snapshot of one node and its edges, taken at each write. The snapshots rebuild any earlier state of the graph.

| Table | Holds | Key |
|---|---|---|
| `node` | One row per node: identity, metadata, content, and vector | `id`, a six-character base62 text identifier |
| `edge` | One row per relation between two nodes | `from_id`, `to_id`, and `relation` together |
| `node_version` | One snapshot per write of a node | `id`, an integer that grows with each write |

Three objects support the tables. The pgvector extension supplies the vector column type. A sequence feeds the ID minter, which turns each number into a base62 identifier. A small function lets the full-text index read the subject array. The full-text index ranks matches in three weights: the title counts most, the subject tags next, and the content least.

## The graph library

The Seele API uses rustworkx, a graph library for Python with its core written in Rust. At startup the API loads every node and edge into a rustworkx directed graph in memory.

Reads walk that in-memory graph, not the database. A tree read follows the `hasPart` edges down from a root, in rank order. Each write lands in PostgreSQL first and updates the graph after the commit. PostgreSQL stays the source of truth.

## Relationships

Every link in the schema points at the node table:

- `edge.from_id` references `node.id`
- `edge.to_id` references `node.id`
- `node_version.node_id` references `node.id`

An edge row needs both of its nodes to exist first. A version row needs its node. A schema load therefore creates the node table before the other two.

## Table definitions

The definitions below run in this order on an empty PostgreSQL database with the pgvector extension available. The setup block comes first, because the node table depends on it.

### setup

```sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE SEQUENCE seele_id_seq START 1;

CREATE OR REPLACE FUNCTION immutable_array_to_string(text[], text)
RETURNS text
LANGUAGE SQL
IMMUTABLE
PARALLEL SAFE
AS $$ SELECT array_to_string($1, $2) $$;
```

### node

```sql
CREATE TABLE node (
    id          TEXT PRIMARY KEY,
    title       TEXT NOT NULL,
    type        TEXT NOT NULL,
    theme       TEXT NOT NULL,
    context     TEXT,
    qualifier   TEXT,
    subject     TEXT[] NOT NULL DEFAULT '{}',
    created     DATE NOT NULL,
    modified    DATE NOT NULL,
    tokens      INTEGER,
    filename    TEXT UNIQUE,
    content     TEXT NOT NULL DEFAULT '',
    vector      vector
);

CREATE INDEX idx_node_filename ON node (filename);
CREATE INDEX idx_node_theme_type ON node (theme, type);
CREATE INDEX idx_node_fts ON node USING GIN ((
    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('english', immutable_array_to_string(subject, ' ')), 'B') ||
    setweight(to_tsvector('english', coalesce(content, '')), 'C')
));
```

### edge

```sql
CREATE TABLE edge (
    from_id     TEXT NOT NULL REFERENCES node(id),
    to_id       TEXT NOT NULL REFERENCES node(id),
    relation    TEXT NOT NULL,
    rank        TEXT,
    PRIMARY KEY (from_id, to_id, relation)
);

CREATE INDEX idx_edge_to ON edge (to_id, relation);
CREATE INDEX idx_edge_from_relation ON edge (from_id, relation);
```

### node_version

```sql
CREATE TABLE node_version (
    id          BIGSERIAL PRIMARY KEY,
    node_id     TEXT NOT NULL REFERENCES node(id),
    written_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    session_id  TEXT NOT NULL,
    node_data   JSONB NOT NULL,
    edge_data   JSONB NOT NULL
);

CREATE INDEX idx_version_node_time ON node_version (node_id, written_at DESC);
```
