Skip to main content
On this page

Graph data model for relationships, navigation, and future RAG capabilities.

Provider: Neo4j via Neo4j Aura (managed) or self-hosted Neo4j Community.

When to Use Neo4j

Use Case Why Graph
Entity relationships Native graph traversal
"Related to" queries Single query vs. recursive SQL
Navigation paths Shortest path algorithms
Recommendations Collaborative filtering
Knowledge graphs Semantic connections
Future RAG Entity extraction + retrieval

Rule: If you're writing recursive CTEs in SQL, consider Neo4j.


Connection Setup

Dependencies

"neo4j-driver": "^5.x"

See development-environment.md for installation workflow.

Client Configuration

// src/lib/server/graph/index.ts
import neo4j, { Driver, Session } from 'neo4j-driver';

let driver: Driver | null = null;

export function getDriver(): Driver {
  if (!driver) {
    driver = neo4j.driver(
      process.env.NEO4J_URI!,
      neo4j.auth.basic(
        process.env.NEO4J_USER!,
        process.env.NEO4J_PASSWORD!
      ),
      {
        // REQUIRED for serverless: Limit pool size for Aura free tier
        maxConnectionPoolSize: 10,
        maxConnectionLifetime: 60 * 60 * 1000, // 1 hour
        connectionAcquisitionTimeout: 60 * 1000, // 1 minute
      }
    );
  }
  return driver;
}

// Connection health check - call on startup or when errors occur
export async function verifyConnection(): Promise<boolean> {
  try {
    const d = getDriver();
    await d.verifyConnectivity();
    return true;
  } catch (error) {
    console.error('Neo4j connection failed:', error);
    // Reset driver to force reconnection on next request
    if (driver) {
      await driver.close();
      driver = null;
    }
    return false;
  }
}

export async function getSession(): Promise<Session> {
  return getDriver().session();
}

export async function closeDriver(): Promise<void> {
  if (driver) {
    await driver.close();
    driver = null;
  }
}

Graceful Shutdown

// src/hooks.server.ts
import { closeDriver } from '$lib/server/graph';

// Close on server shutdown
process.on('SIGTERM', async () => {
  await closeDriver();
  process.exit(0);
});

Graph Model

Node Types

// Page nodes (template documentation)
(:Page {
  id: string,
  path: string,      // '/showcase/theme'
  title: string,
  category: string   // 'showcase' | 'app' | 'auth' | 'docs'
})

// Feature nodes (stack components)
(:Feature {
  id: string,
  name: string,      // 'Drizzle', 'UnoCSS', 'Better Auth'
  category: string   // 'auth' | 'database' | 'styling' | 'validation'
})

// Concept nodes (for future RAG)
(:Concept {
  id: string,
  name: string,
  description: string
})

Relationship Types

// Page relationships
(:Page)-[:USES]->(:Feature)           // Page uses this stack feature
(:Page)-[:RELATES_TO]->(:Page)        // Related pages
(:Page)-[:REQUIRES]->(:Page)          // Must understand this first

// Feature relationships
(:Feature)-[:DEPENDS_ON]->(:Feature)  // Feature dependency
(:Feature)-[:IMPLEMENTS]->(:Concept)  // Feature implements concept

// Navigation
(:Page)-[:NEXT]->(:Page)              // Suggested reading order

TypeScript Types

// src/lib/server/graph/types.ts
export interface PageNode {
  id: string;
  path: string;
  title: string;
  category: 'showcase' | 'app' | 'auth' | 'docs';
}

export interface FeatureNode {
  id: string;
  name: string;
  category: 'auth' | 'database' | 'styling' | 'validation' | 'runtime';
}

export interface ConceptNode {
  id: string;
  name: string;
  description: string;
}

export type RelationType =
  | 'USES'
  | 'RELATES_TO'
  | 'REQUIRES'
  | 'DEPENDS_ON'
  | 'IMPLEMENTS'
  | 'NEXT';

Query Helpers

// src/lib/server/graph/queries.ts
import { getSession } from './index';
import type { PageNode, FeatureNode } from './types';

export async function getPageFeatures(pagePath: string): Promise<FeatureNode[]> {
  const session = await getSession();
  try {
    const result = await session.run(
      `
      MATCH (p:Page {path: $path})-[:USES]->(f:Feature)
      RETURN f
      `,
      { path: pagePath }
    );
    return result.records.map(r => r.get('f').properties as FeatureNode);
  } finally {
    await session.close();
  }
}

export async function getPagesUsingFeature(featureName: string): Promise<PageNode[]> {
  const session = await getSession();
  try {
    const result = await session.run(
      `
      MATCH (p:Page)-[:USES]->(f:Feature {name: $name})
      RETURN p
      `,
      { name: featureName }
    );
    return result.records.map(r => r.get('p').properties as PageNode);
  } finally {
    await session.close();
  }
}

export async function getRelatedPages(pagePath: string): Promise<PageNode[]> {
  const session = await getSession();
  try {
    const result = await session.run(
      `
      MATCH (p:Page {path: $path})-[:RELATES_TO|REQUIRES]-(related:Page)
      RETURN DISTINCT related
      `,
      { path: pagePath }
    );
    return result.records.map(r => r.get('related').properties as PageNode);
  } finally {
    await session.close();
  }
}

export async function getReadingPath(startPath: string, depth: number = 5): Promise<PageNode[]> {
  const session = await getSession();
  try {
    const result = await session.run(
      `
      MATCH path = (start:Page {path: $path})-[:NEXT*1..${depth}]->(end:Page)
      RETURN [node IN nodes(path) | node] AS pages
      ORDER BY length(path) DESC
      LIMIT 1
      `,
      { path: startPath }
    );
    if (result.records.length === 0) return [];
    return result.records[0].get('pages').map((n: any) => n.properties as PageNode);
  } finally {
    await session.close();
  }
}

export async function getFeatureDependencies(featureName: string): Promise<FeatureNode[]> {
  const session = await getSession();
  try {
    const result = await session.run(
      `
      MATCH (f:Feature {name: $name})-[:DEPENDS_ON*]->(dep:Feature)
      RETURN DISTINCT dep
      `,
      { name: featureName }
    );
    return result.records.map(r => r.get('dep').properties as FeatureNode);
  } finally {
    await session.close();
  }
}

Write Operations

// src/lib/server/graph/mutations.ts
import { getSession } from './index';
import { nanoid } from 'nanoid';

export async function createPage(
  path: string,
  title: string,
  category: string
): Promise<string> {
  const session = await getSession();
  const id = `pg_${nanoid(8)}`;
  try {
    await session.run(
      `
      CREATE (p:Page {id: $id, path: $path, title: $title, category: $category})
      RETURN p
      `,
      { id, path, title, category }
    );
    return id;
  } finally {
    await session.close();
  }
}

export async function linkPageToFeature(
  pagePath: string,
  featureName: string
): Promise<void> {
  const session = await getSession();
  try {
    await session.run(
      `
      MATCH (p:Page {path: $path}), (f:Feature {name: $name})
      MERGE (p)-[:USES]->(f)
      `,
      { path: pagePath, name: featureName }
    );
  } finally {
    await session.close();
  }
}

export async function setNextPage(
  fromPath: string,
  toPath: string
): Promise<void> {
  const session = await getSession();
  try {
    await session.run(
      `
      MATCH (from:Page {path: $from}), (to:Page {path: $to})
      MERGE (from)-[:NEXT]->(to)
      `,
      { from: fromPath, to: toPath }
    );
  } finally {
    await session.close();
  }
}

Seed Data

// scripts/seed-graph.ts
import { getSession, closeDriver } from '$lib/server/graph';

async function seedGraph() {
  const session = await getSession();

  try {
    // Clear existing data (dev only!)
    await session.run('MATCH (n) DETACH DELETE n');

    // Create features
    await session.run(`
      CREATE (:Feature {id: 'ft_unocss', name: 'UnoCSS', category: 'styling'})
      CREATE (:Feature {id: 'ft_bitsui', name: 'Bits UI', category: 'components'})
      CREATE (:Feature {id: 'ft_superforms', name: 'Superforms', category: 'forms'})
      CREATE (:Feature {id: 'ft_valibot', name: 'Valibot', category: 'validation'})
      CREATE (:Feature {id: 'ft_drizzle', name: 'Drizzle', category: 'database'})
      CREATE (:Feature {id: 'ft_betterauth', name: 'Better Auth', category: 'auth'})
    `);

    // Create pages
    await session.run(`
      CREATE (:Page {id: 'pg_theme', path: '/showcase/theme', title: 'Theme', category: 'showcase'})
      CREATE (:Page {id: 'pg_ui', path: '/showcase/ui', title: 'UI Components', category: 'showcase'})
      CREATE (:Page {id: 'pg_forms', path: '/showcase/forms', title: 'Forms', category: 'showcase'})
      CREATE (:Page {id: 'pg_data', path: '/showcase/data', title: 'Data', category: 'showcase'})
      CREATE (:Page {id: 'pg_auth', path: '/auth/login', title: 'Login', category: 'auth'})
    `);

    // Create relationships
    await session.run(`
      MATCH (p:Page {path: '/showcase/theme'}), (f:Feature {name: 'UnoCSS'})
      CREATE (p)-[:USES]->(f)
    `);

    await session.run(`
      MATCH (p:Page {path: '/showcase/ui'}), (f:Feature {name: 'Bits UI'})
      CREATE (p)-[:USES]->(f)
    `);

    await session.run(`
      MATCH (p:Page {path: '/showcase/forms'}), (f1:Feature {name: 'Superforms'}), (f2:Feature {name: 'Valibot'})
      CREATE (p)-[:USES]->(f1), (p)-[:USES]->(f2)
    `);

    await session.run(`
      MATCH (f1:Feature {name: 'Superforms'}), (f2:Feature {name: 'Valibot'})
      CREATE (f1)-[:DEPENDS_ON]->(f2)
    `);

    // Reading order
    await session.run(`
      MATCH (p1:Page {path: '/showcase/theme'}), (p2:Page {path: '/showcase/ui'}), (p3:Page {path: '/showcase/forms'})
      CREATE (p1)-[:NEXT]->(p2)-[:NEXT]->(p3)
    `);

    console.log('Graph seed complete');
  } finally {
    await session.close();
    await closeDriver();
  }
}

seedGraph();

API Integration

// src/routes/api/graph/features/+server.ts
import { json } from '@sveltejs/kit';
import { getPagesUsingFeature } from '$lib/server/graph/queries';

export async function GET({ url }) {
  const name = url.searchParams.get('name');
  if (!name) {
    return json({ error: 'Feature name required' }, { status: 400 });
  }

  const pages = await getPagesUsingFeature(name);
  return json({ pages });
}
// src/routes/showcase/[slug]/+page.server.ts
import { getPageFeatures, getRelatedPages } from '$lib/server/graph/queries';

export async function load({ params }) {
  const path = `/showcase/${params.slug}`;

  const [features, related] = await Promise.all([
    getPageFeatures(path),
    getRelatedPages(path)
  ]);

  return { features, related };
}

Useful Queries

Find All Features a Page Uses

MATCH (p:Page {path: '/showcase/forms'})-[:USES]->(f:Feature)
RETURN f.name, f.category

Find All Pages Using a Feature

MATCH (p:Page)-[:USES]->(f:Feature {name: 'Valibot'})
RETURN p.path, p.title

Feature Dependency Tree

MATCH path = (f:Feature {name: 'Superforms'})-[:DEPENDS_ON*]->(dep:Feature)
RETURN path

Suggested Reading Path

MATCH path = (start:Page {path: '/showcase/theme'})-[:NEXT*1..5]->(end:Page)
RETURN [node IN nodes(path) | node.title] AS readingOrder

Pages Sharing Features (Recommendations)

MATCH (p1:Page {path: '/showcase/forms'})-[:USES]->(f:Feature)<-[:USES]-(p2:Page)
WHERE p1 <> p2
RETURN p2.path, p2.title, count(f) AS sharedFeatures
ORDER BY sharedFeatures DESC
LIMIT 5

Find Prerequisite Pages

MATCH path = (target:Page {path: '/showcase/forms'})<-[:REQUIRES*]-(prereq:Page)
RETURN [node IN nodes(path) | node.title] AS prerequisites

Constraints & Indexes

Run once on database setup:

// Unique constraints
CREATE CONSTRAINT page_path_unique IF NOT EXISTS
FOR (p:Page) REQUIRE p.path IS UNIQUE;

CREATE CONSTRAINT feature_name_unique IF NOT EXISTS
FOR (f:Feature) REQUIRE f.name IS UNIQUE;

// Indexes for faster lookups
CREATE INDEX page_category IF NOT EXISTS
FOR (p:Page) ON (p.category);

CREATE INDEX feature_category IF NOT EXISTS
FOR (f:Feature) ON (f.category);

Environment Variables

# Local (container)
NEO4J_URI="bolt://localhost:7687"
NEO4J_USER="neo4j"
NEO4J_PASSWORD="password"

# Production (Neo4j Aura)
NEO4J_URI="neo4j+s://xxxxxxxx.databases.neo4j.io"
NEO4J_USER="neo4j"
NEO4J_PASSWORD="your-aura-password"

File Structure

src/lib/server/graph/
├── index.ts      # Driver connection
├── types.ts      # Node/relationship types
├── queries.ts    # Read operations
└── mutations.ts  # Write operations

scripts/
└── seed-graph.ts # Development seeding

← Back to Blueprint