Skip to main content
Better Skills models the knowledge base as a directed graph where skills and resources are nodes, and links are typed edges. The graph spans multiple vaults and supports polymorphic connections.

Graph Structure

1

Skills are primary nodes

Each skill in the database is a node in the graph, identified by its UUID.
2

Resources are sub-nodes

Resource files (references, scripts, assets) are child nodes under their parent skill.
3

Links are directed edges

skill_link rows represent typed, directed connections between any pair of nodes.

Node Types

Skills as Nodes

Every skill is a graph node with:
  • Identity: UUID primary key
  • Vault scope: Belongs to exactly one vault
  • Metadata: Name, description, frontmatter
  • Content: SKILL.md markdown
  • Child nodes: Zero or more resource sub-nodes

Resources as Sub-Nodes

Resources are nested nodes within their parent skill:
CREATE TABLE skill_resource (
  id UUID PRIMARY KEY,
  skill_id UUID NOT NULL REFERENCES skill(id) ON DELETE CASCADE,
  path TEXT NOT NULL,
  kind skill_resource_kind NOT NULL, -- reference | script | asset | other
  content TEXT NOT NULL,
  UNIQUE(skill_id, path)
);

Reference

Markdown documentation files under references/.

Script

Executable code samples under scripts/.

Asset

Binary or config files under assets/.

Other

Miscellaneous files that don’t fit other categories.
Resources are cascade deleted when their parent skill is deleted. They cannot exist independently.
The skill_link table represents polymorphic directed edges between nodes:
export const skillLink = pgTable("skill_link", {
  id: uuid("id").defaultRandom().primaryKey(),
  // Source: exactly one of these
  sourceSkillId: uuid("source_skill_id").references(() => skill.id, { onDelete: "cascade" }),
  sourceResourceId: uuid("source_resource_id").references(() => skillResource.id, { onDelete: "cascade" }),
  // Target: exactly one of these
  targetSkillId: uuid("target_skill_id").references(() => skill.id, { onDelete: "cascade" }),
  targetResourceId: uuid("target_resource_id").references(() => skillResource.id, { onDelete: "cascade" }),
  // Edge metadata
  kind: text("kind").notNull().default("related"),
  note: text("note"),
  metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
  createdByUserId: text("created_by_user_id").references(() => user.id, { onDelete: "set null" }),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Polymorphic Source and Target

Each link has:
  • Exactly one source: Either source_skill_id OR source_resource_id
  • Exactly one target: Either target_skill_id OR target_resource_id
This enables four link shapes:
INSERT INTO skill_link (source_skill_id, target_skill_id, kind)
VALUES ('turborepo-uuid', 'next-best-practices-uuid', 'related');
Database constraints enforce that exactly one source and one target are set. Attempting to set both source_skill_id and source_resource_id will fail.
The kind field categorizes the relationship:
KindDescriptionTypical Use
relatedGeneral associationTopically connected skills
depends_onFunctional dependencySkill requires another skill’s context
mentionMarkdown referenceAuto-generated from [[skill:uuid]] mentions
extendsInheritance relationshipSkill builds on another skill
supersedesVersion replacementNewer skill replaces older one
The mention kind is automatically created when markdown content contains [[skill:uuid]] or [[resource:uuid]] syntax. See Mentions for details.
Each link can store additional context:
interface LinkMetadata {
  origin?: "markdown-auto" | "manual-ui" | "api-import";
  confidence?: number;  // For auto-detected links
  context?: string;     // Surrounding text snippet
  [key: string]: unknown;
}

Auto Links

Created automatically from markdown mentions.
  • metadata.origin: "markdown-auto"
  • Recreated on skill update
  • Deleted when mention removed

Manual Links

Explicitly created by users via UI or API.
  • metadata.origin: "manual-ui"
  • Persist independently of markdown
  • Can have custom notes and types
Duplicate edges are allowed by design. A skill can have multiple links to the same target with different kind values or metadata.

Graph Traversal

Find all skills and resources this skill links to:
SELECT 
  sl.kind,
  sl.note,
  COALESCE(target_skill.name, target_resource.path) AS target_name
FROM skill_link sl
LEFT JOIN skill target_skill ON sl.target_skill_id = target_skill.id
LEFT JOIN skill_resource target_resource ON sl.target_resource_id = target_resource.id
WHERE sl.source_skill_id = 'my-skill-uuid';
Find all skills and resources that link to this skill:
SELECT 
  sl.kind,
  COALESCE(source_skill.name, source_resource.path) AS source_name
FROM skill_link sl
LEFT JOIN skill source_skill ON sl.source_skill_id = source_skill.id
LEFT JOIN skill_resource source_resource ON sl.source_resource_id = source_resource.id
WHERE sl.target_skill_id = 'my-skill-uuid';

Multi-Hop Traversal

Recursive queries can traverse the graph across multiple hops:
WITH RECURSIVE skill_graph AS (
  -- Base: direct links from starting skill
  SELECT 
    target_skill_id AS skill_id,
    1 AS depth
  FROM skill_link
  WHERE source_skill_id = 'starting-skill-uuid'
    AND target_skill_id IS NOT NULL
  
  UNION ALL
  
  -- Recursive: follow links from previously found skills
  SELECT 
    sl.target_skill_id,
    sg.depth + 1
  FROM skill_graph sg
  JOIN skill_link sl ON sg.skill_id = sl.source_skill_id
  WHERE sl.target_skill_id IS NOT NULL
    AND sg.depth < 3  -- Limit depth
)
SELECT DISTINCT s.name, sg.depth
FROM skill_graph sg
JOIN skill s ON sg.skill_id = s.id
ORDER BY sg.depth, s.name;
Graph traversal respects vault boundaries. Links can reference skills in any vault the user has access to.

Cascade Deletion

Graph integrity is maintained through cascade rules:
1

Deleting a skill

  • All child resources are deleted (ON DELETE CASCADE)
  • All incoming and outgoing links are deleted
2

Deleting a resource

  • All incoming and outgoing links from that resource are deleted
  • Parent skill is unaffected
3

Deleting a user

  • created_by_user_id in links is set to NULL (ON DELETE SET NULL)
  • Links themselves are preserved

Same-Vault Validation

When creating links from markdown mentions, the system enforces same-vault constraints:
// From packages/api/src/lib/link-sync.ts
const sourceVaultId = await getVaultIdForNode(sourceSkillId, sourceResourceId);
const targetVaultId = await getVaultIdForNode(targetSkillId, targetResourceId);

if (sourceVaultId !== targetVaultId) {
  throw new Error(
    `Cross-vault mentions not allowed: source in ${sourceVaultId}, target in ${targetVaultId}`
  );
}
Manual links created via UI/API can span vaults. The same-vault rule applies only to markdown-auto mentions.

Graph API

The tRPC API exposes graph operations:
// Get graph centered on a skill
const graph = await trpc.skills.getGraph.query({
  skillId: 'my-skill-uuid',
  depth: 2, // How many hops to traverse
  includeResources: true,
});

// Graph response structure
interface SkillGraph {
  nodes: {
    skills: { id: string; name: string; vaultId: string }[];
    resources: { id: string; path: string; skillId: string }[];
  };
  edges: {
    id: string;
    source: { type: 'skill' | 'resource'; id: string };
    target: { type: 'skill' | 'resource'; id: string };
    kind: string;
    note?: string;
  }[];
}

Next Steps

Mentions

Learn how markdown mentions create auto links

Skills

Understand skill structure and resources

Vaults

Review vault types and permissions

Build docs developers (and LLMs) love