Resource Model
A resource is any entity in the graph: a user, team, document, predicate, template definition, or module-installed object. All resources share a single table and are distinguished by their kind.
Table Schema
CREATE TABLE graph.resource (
id BIGINT PRIMARY KEY DEFAULT ids.gen_resource_id(),
kind LTREE NOT NULL,
external_id TEXT,
data JSONB NOT NULL DEFAULT '{}',
creator_id BIGINT REFERENCES graph.resource (id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ,
CONSTRAINT unique_resource UNIQUE (kind, external_id)
);
Columns
id — Snowflake ID
Every resource gets a BIGINT primary key generated by ids.gen_resource_id(), a PostgreSQL function that produces Snowflake-compatible IDs. These are time-ordered, globally unique, and safe for distributed ID generation. System bootstrap reserves low ID ranges for well-known resources (see Reserved IDs).
kind — Operational Classifier
kind is the single canonical classifier for a resource. It is an ltree (dot-separated hierarchical path) that associates the resource with the module that installed it and determines operational dispatch and storage behavior.
Examples:
graph.resource— a resource type definitiongraph.predicate_policy— a permission policycore.sdo.Person— a schema.org Person typeteams.member— a team membership predicate
The ltree grammar constrains segments to lowercase alphanumeric characters and underscores. Module namespaces map directly to kind prefixes — a module with namespace: teams installs resources with kind values under the teams.* subtree.
The legacy type_namespace column alias has been fully retired from the codebase. kind is the only classifier.
external_id — Human-Readable Identifier
An optional text identifier for looking up resources by name within a kind. The UNIQUE (kind, external_id) constraint guarantees that each (kind, external_id) pair is unique. This is the primary mechanism for idempotent resource creation: re-inserting a resource with the same kind and external_id can be handled via ON CONFLICT.
data — JSONB Payload
Flexible JSON data whose structure depends on the resource's kind and the application code. Defaults to '{}'::jsonb. JSONB indexing (@> containment, GIN indexes) enables efficient querying within the data payload.
creator_id — Audit Trail
References the resource that created this one (typically a user resource). Immutable after creation. Set to NULL on creator deletion (ON DELETE SET NULL).
Timestamps
created_at— Immutable creation timestampupdated_at— Updated on modificationdeleted_at— Soft-delete marker;NULLwhen active
Common Patterns
Idempotent Creation
INSERT INTO graph.resource (kind, external_id, data)
VALUES ('teams.team', 'engineering', '{"name": "Engineering"}')
ON CONFLICT (kind, external_id) DO UPDATE SET
data = EXCLUDED.data,
updated_at = now();
Query by Kind Subtree
-- All resources installed by the 'teams' module
SELECT * FROM graph.resource WHERE kind <@ 'teams';
-- All schema.org types
SELECT * FROM graph.resource WHERE kind <@ 'core.sdo';
Lookup by External ID
SELECT * FROM graph.resource
WHERE kind = 'auth.user' AND external_id = 'alice';
TypeScript Mapping
The graph-model package provides typed resource schemas:
import { resourceSchema } from '@savvi-studio/graph-model';
Key mapped fields: id (bigint), kind (string), externalId (string | null), data (any), creatorId (bigint | null).
See Also
- Graph Architecture — System overview
- Statement Model — Relationships between resources
- Permissions — Access control model
- Reserved IDs — System bootstrap ID ranges