Fair Supply LogoFair Supply - Docs

Neo4j Infrastructure

Neo4j is the primary database for the FSA Platform, using @neo4j/graphql for schema-driven development.

Schema Location

All schemas: packages/core/src/infrastructure/neo4j/schemas/

Schema Template

type Entity @node {
  id: ID! @id
  name: String!
  description: String
  status: String! @default(value: "draft")
  # Uniqueness enforced via DB constraints (see migrations/)

  # Relationships
  parent: Parent! @relationship(type: "BELONGS_TO", direction: OUT)
  children: [Child!]! @relationship(type: "HAS", direction: OUT)

  # Computed (needs custom resolver)
  computed: Boolean @customResolver

  # Timestamps
  createdAt: DateTime! @timestamp(operations: [CREATE])
  updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE])
}

Directives Reference

DirectivePurposeExample
@nodeMarks as Neo4j nodetype User @node
@idAuto-generate unique IDid: ID! @id
@defaultDefault valuestatus: String! @default(value: "draft")
@timestampAuto-manage datescreatedAt: DateTime! @timestamp(operations: [CREATE])
@relationshipDefine graph edgeSee below
@customResolverNeeds custom resolvercomputed: Boolean @customResolver

Note: @unique was removed in @neo4j/graphql v7. Uniqueness constraints are managed via Cypher migrations in packages/core/src/infrastructure/neo4j/migrations/.

Relationship Patterns

Simple Relationship

organisation: Organisation! @relationship(type: "FOR", direction: OUT)

With Edge Properties

type UserAccountEdge @relationshipProperties {
  role: String!
  joinedAt: DateTime!
}

type Account @node {
  users: [User!]! @relationship(type: "MEMBER_OF", direction: IN, properties: "UserAccountEdge")
}

Many-to-Many

# Both sides reference the same relationship type
frameworks: [Framework!]! @relationship(type: "USES", direction: OUT)
engagements: [Engagement!]! @relationship(type: "USES", direction: IN)

Custom Resolvers

Location: packages/core/src/infrastructure/neo4j/resolvers/

import { getDriver } from '../client';

export const organisationResolvers = {
  Organisation: {
    provisional: async (parent: { id: string }) => {
      const driver = getDriver();
      const session = driver.session();
      try {
        const result = await session.run(
          `MATCH (o:Organisation {id: $id}) RETURN o:Provisional as provisional`,
          { id: parent.id }
        );
        return result.records[0]?.get('provisional') ?? false;
      } finally {
        await session.close();
      }
    },
  },
};

Cypher Executor

For custom Cypher queries: packages/core/src/infrastructure/neo4j/cypher-executor.ts

import { executeCypher } from '../neo4j/cypher-executor';

// Read query
const result = await executeCypher(
  `MATCH (o:Organisation)-[:HAS]->(l:Location) WHERE o.id = $id RETURN l`,
  { id: organisationId },
  { accessMode: 'READ' }
);

// Write query
await executeCypher(
  `MATCH (o:Organisation {id: $id}) SET o.name = $name RETURN o`,
  { id, name },
  { accessMode: 'WRITE' }
);

Naming Conventions

TypeConventionExample
Node LabelsPascalCaseUser, Organisation
Relationship TypesSCREAMING_SNAKE_CASEMEMBER_OF, BELONGS_TO
PropertiescamelCasecreatedAt, firstName

After Schema Changes

pnpm codegen  # Regenerate types

Best Practices

  1. Use directives - @timestamp, @id, @default over manual handling
  2. Define relationships explicitly - clear direction and type
  3. Edge properties for metadata - role, timestamp on relationships
  4. Custom resolvers sparingly - only when directives don't suffice
  5. Index important fields - for query performance

Connection & Pool Tuning

The driver's pool size and connection timeouts follow the env-var convention (ADR-0012, docs/adr/0012-local-dev-env-var-fallbacks.md): the in-code defaults are tuned for local dev, and preview/production override them on Vercel for Fluid Compute against Aura.

Driver settingEnv varLocal defaultPreview / production
maxConnectionPoolSizeNEO4J_MAX_POOL_SIZE100set on Vercel (currently 40)
connectionAcquisitionTimeoutNEO4J_CONNECTION_ACQUISITION_TIMEOUT_MS60000optional override
connectionLivenessCheckTimeoutNEO4J_CONNECTION_LIVENESS_CHECK_TIMEOUT_MS30000optional override
maxConnectionLifetimeNEO4J_MAX_CONNECTION_LIFETIME_MS480000optional override

Set NEO4J_MAX_POOL_SIZE on Vercel before deploying — its default changed (was a hardcoded 10), so set it (Production + Preview) or a deploy inherits the local-best 100 and many instances can exhaust the Aura connection ceiling. The timeout vars are safe to leave unset — connectionAcquisitionTimeout is now intentionally 60s in every environment (was 10s); override only to tune.

All four resolve through resolveDriverConfig() in client.ts via the shared readPositiveInt reader (packages/core/src/infrastructure/config/env.ts) — each a positive integer (milliseconds for the timeouts), warning and falling back on an invalid value.

TypeLocation
Schemaspackages/core/src/infrastructure/neo4j/schemas/
Clientpackages/core/src/infrastructure/neo4j/client.ts
Env config readerpackages/core/src/infrastructure/config/env.ts
Resolverspackages/core/src/infrastructure/neo4j/resolvers/
Cypher Executorpackages/core/src/infrastructure/neo4j/cypher-executor.ts

On this page