neo4j-contrib/neo4j-skills3 files

Neo4j GraphQL Skill

Build and configure a GraphQL API backed by Neo4j using @neo4j/graphql v7 (current) or v5 (LTS).

Specification
Skill ID
neo4j-contrib/neo4j-skills/neo4j-graphql-skill
Publisher
neo4j-contrib
Repository
neo4j-skills
Installs
0
Files
3
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

neo4j-contrib/neo4j-skills/neo4j-graphql-skillInstalls these files
  • README.md
  • SKILL.md
  • references/search-directives.md

What this skill tells the agent

When to Use

  • Creating a GraphQL API from a Neo4j graph schema with @neo4j/graphql
  • Writing type definitions with @relationship, @cypher, @authorization directives
  • Using OGM for server-side programmatic Neo4j access (bypasses GraphQL auth)
  • Configuring auto-generated queries, mutations, subscriptions
  • Securing types/fields with JWT or JWKS-based @authorization rules
  • Migrating from v5/v6 to v7 (breaking changes below)

When NOT to Use

  • Raw Cypher queries outside GraphQL resolvers → neo4j-cypher-skill
  • Spring Data Neo4j / Java entity mapping → neo4j-spring-data-skill
  • Generic GraphQL without Neo4j — outside scope

Version Matrix

VersionStatusNotes
v7Current — use ≥ 7.6.2@node required; options removed; explicit eq syntax
v5LTS — use ≥ 5.12.15Older syntax; options: {limit, offset, sort} still valid

Default to v7 unless codebase is on v5. Upgrade past these security patches before shipping auth:

FixFixed in
Subscriptions accepted an unverified JWT from WebSocket connectionParams (auth/authz bypass)7.5.6 / 5.12.14
Field-level @authentication on a root custom-resolver field ignored when the operation type also had type-level @authentication (privilege escalation)7.6.0 / 5.12.15
@authorization rules not applied on create-relationship operations7.6.2

Step 1 — Install

npm install @neo4j/graphql neo4j-driver graphql @apollo/server

For subscriptions (CDC required):

npm install ws graphql-ws express body-parser cors

Step 2 — Minimal Server Setup

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j from 'neo4j-driver';

const typeDefs = `#graphql
  type Movie @node {
    id: ID! @id
    title: String!
    actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN)
  }

  type Person @node {
    id: ID! @id
    name: String!
    movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
  }
`;

const driver = neo4j.driver(
  process.env.NEO4J_URI,
  neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD)
);

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });

// assertIndexesAndConstraints syncs @id → UNIQUE constraints; wrap in try/catch
await neoSchema.assertIndexesAndConstraints({ options: { create: true } });

const server = new ApolloServer({ schema: await neoSchema.getSchema() });

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({ token: req.headers.authorization }),
  listen: { port: 4000 },
});

Key Directives

@node (v7 required)

type Product @node {
  id: ID! @id
  name: String!
}

# Custom label (default = type name)
type Article @node(labels: ["Post", "Content"]) {
  title: String!
}

@relationship — Full Syntax

type Person @node {
  # direction: OUT = (this)-[:KNOWS]->(other)
  friends: [Person!]! @relationship(type: "KNOWS", direction: OUT)

  # direction: IN = (other)-[:ACTED_IN]->(this)
  actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: IN)

  # direction: UNDIRECTED = matches both directions (use sparingly — double-counts)
  colleagues: [Person!]! @relationship(type: "COLLEAGUE_OF", direction: UNDIRECTED)

  # Relationship with properties — reference an @relationshipProperties interface
  reviews: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "ReviewedProps")
}

interface ReviewedProps @relationshipProperties {
  rating: Int!
  date: Date
}

Querying Relationship Properties — Connection API

For each relationship with properties:, a {field}Connection field is auto-generated. Access rel properties via actorsConnection.edges.properties, not via actors:

query {
  movies(where: { title: { eq: "The Matrix" } }) {
    title
    actorsConnection {
      edges {
        properties { role }   # maps to @relationshipProperties interface
        node { name }
      }
    }
  }
}

@cypher — Custom Resolver

type Person @node {
  name: String!

  # columnName must exactly match the RETURN alias — mismatch returns null silently
  friendCount: Int
    @cypher(
      statement: "MATCH (this)-[:KNOWS]->(f:Person) RETURN count(f) AS friendCount"
      columnName: "friendCount"
    )

  recommendedMovies: [Movie!]!
    @cypher(
      statement: """
        MATCH (this)-[:WATCHED]->(m:Movie)<-[:WATCHED]-(o:Person)-[:WATCHED]->(rec:Movie)
        WHERE NOT (this)-[:WATCHED]->(rec)
        RETURN rec
      """
      columnName: "rec"
    )
}

# @cypher on Query field — custom top-level query
type Query {
  topRatedMovies(limit: Int = 10): [Movie!]!
    @cypher(
      statement: "MATCH (m:Movie) WHERE m.rating IS NOT NULL RETURN m ORDER BY m.rating DESC LIMIT $limit"
      columnName: "m"
    )
}

this refers to the current node in field-level @cypher. Parameters are passed as $paramName.

@cypher — Field Arguments and extend type

# extend type adds computed fields without modifying the base type definition
extend type Movie @node {
  avgRating: Float
    @cypher(statement: "MATCH (this)<-[r:RATED]-(:User) RETURN avg(r.rating) AS result", columnName: "result")

  # Field arguments passed as Cypher params; always provide default to avoid null
  recommended(limit: Int = 3): [Movie!]!
    @cypher(
      statement: "MATCH (this)<-[:RATED]-(u:User)-[:RATED]->(rec:Movie) WITH rec, COUNT(u) AS score ORDER BY score DESC RETURN rec LIMIT $limit"
      columnName: "rec"
    )
}

@id and @timestamp

type Post @node {
  id: ID! @id                          # auto-generates UUID; creates UNIQUE constraint
  createdAt: DateTime! @timestamp(operations: [CREATE])
  updatedAt: DateTime @timestamp(operations: [CREATE, UPDATE])
  title: String!
}

@alias — Map GraphQL field to Neo4j property

type User @node {
  id: ID! @id
  email: String! @alias(property: "emailAddress")  # GraphQL: email → DB: emailAddress
}

@fulltext and @vector

Index must exist in Cypher before use. Phrase-capable @vector indexes accept maxPhraseLength [7.6.0] to cap embedding cost. A provider without matching features.vector config fails at schema build [7.6.1]. Full syntax, generated query shapes, provider config: references/search-directives.md.


Security — @authentication and @authorization

Step 1: Configure JWT in constructor

// Symmetric secret
const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
  features: {
    authorization: { key: process.env.JWT_SECRET },
  },
});

// JWKS endpoint (production)
const neoSchema = new Neo4jGraphQL({
  typeDefs,
  driver,
  features: {
    authorization: {
      key: { url: 'https://myapp.com/.well-known/jwks.json' },
    },
  },
});

Step 2: Pass token in context

context: async ({ req }) => ({ token: req.headers.authorization }),
// Or pass pre-decoded JWT:
context: async ({ req }) => ({ jwt: myDecodeJwt(req.headers.authorization) }),

Step 3: Apply @authentication and @authorization

# Require auth on all operations for a type
type Post @node
  @authentication
  @authorization(filter: [{ where: { node: { author: { id: { eq: "$jwt.sub" } } } } }]) {
  title: String!
  author: User! @relationship(type: "AUTHORED", direction: IN)
}

# requireAuthentication: false = allow public access without JWT
type Article @node
  @authorization(filter: [
    { requireAuthentication: false, where: { node: { published: { eq: true } } } }
    { where: { node: { author: { id: { eq: "$jwt.sub" } } } } }
  ]) {
  title: String!
  published: Boolean!
}

# validate (throws error) vs filter (silently hides data)
type BankAccount @node
  @authorization(validate: [{

Use this skill in a project

Open any RiverX project, search the Skills panel for neo4j-contrib/neo4j-skills/neo4j-graphql-skill, and install it in one click.

Use this skill