| ← Back to Main Documentation | Core Systems Index |
Spatial Relationships System
Created: Original date unknown
Updated: Tue 29 Jul 2025 07:36:45 CEST - Renamed from spatial-relationships-system.md and added Jekyll front matter
Document Version: 1.1 - Updated during file structure reorganization
Security Classification: Internal Technical Documentation
Target Audience: Backend Developers, Database Engineers, System Architects
Author: Paul Wisén
This document defines how objects relate to each other in physical space using flexible, predicate-based relationships. The spatial system tracks both where objects are (current location) and where they should be (expected location) to enable inventory management and misplacement detection.
Core Concepts
Spatial Predicates
Relationships like IN, ON, NEXT_TO, etc. that describe physical positioning between objects. The available predicates are defined in the Supabase spatial_predicates table as the single source of truth, and are stored as directed relationships in Neo4j.
Predicate Architecture
- Supabase
spatial_predicatestable: Defines all available spatial relationship types - Neo4j graph relationships: Stores actual relationship instances between objects
- GraphQL API: Bridges predicate definitions with relationship data
Dual Spatial Predicate System (from Supabase)
The system uses dual predicates to separately track current and expected/normal locations with full spatial detail:
CURRENT_*predicates: Where objects are nowNORMAL_*predicates: Where objects should be / are expected to be
Containment Predicates (axis: containment)
CURRENT_IN/NORMAL_IN: Subject is/should be inside target- Inverse:
CURRENT_CONTAINS/NORMAL_CONTAINS - Creates parent relationship:
true - Example:
(:Keys)-[:CURRENT_IN]->(:Drawer),(:Keys)-[:NORMAL_IN]->(:KeyHook)
- Inverse:
CURRENT_CONTAINS/NORMAL_CONTAINS: Target contains/should contain subject- Inverse:
CURRENT_IN/NORMAL_IN - Creates parent relationship:
true - Example:
(:Drawer)-[:CURRENT_CONTAINS]->(:Keys)
- Inverse:
Support Predicates (axis: support)
CURRENT_ON/NORMAL_ON: Subject is/should be on top of target- No inverse (unidirectional)
- Creates parent relationship:
true - Example:
(:Box)-[:CURRENT_ON]->(:Pallet_A),(:Box)-[:NORMAL_ON]->(:Pallet_B)
Horizontal Positioning Predicates (axis: horizontal)
CURRENT_LEFT_OF/NORMAL_LEFT_OF: Subject is/should be left of target- Inverse:
CURRENT_RIGHT_OF/NORMAL_RIGHT_OF - Creates parent relationship:
false - Example:
(:Monitor)-[:CURRENT_LEFT_OF]->(:Computer),(:Monitor)-[:NORMAL_RIGHT_OF]->(:Computer)
- Inverse:
CURRENT_RIGHT_OF/NORMAL_RIGHT_OF: Subject is/should be right of target- Inverse:
CURRENT_LEFT_OF/NORMAL_LEFT_OF - Creates parent relationship:
false
- Inverse:
CURRENT_NEXT_TO/NORMAL_NEXT_TO: Subject is/should be next to target- Symmetric: No inverse (bidirectional)
- Creates parent relationship:
false
Vertical Positioning Predicates (axis: vertical)
CURRENT_ABOVE/NORMAL_ABOVE: Subject is/should be above target- Inverse:
CURRENT_UNDER/NORMAL_UNDER - Creates parent relationship:
false
- Inverse:
CURRENT_UNDER/NORMAL_UNDER: Subject is/should be under/below target- Inverse:
CURRENT_ABOVE/NORMAL_ABOVE - Creates parent relationship:
false
- Inverse:
Attachment Predicates (axis: attachment)
CURRENT_ATTACHED_TO/NORMAL_ATTACHED_TO: Subject is/should be attached to target- Symmetric: No inverse (bidirectional)
- Creates parent relationship:
false
Legacy Predicate (Deprecated)
SHOULD_BE_IN: Legacy expected location predicate (replaced byNORMAL_*predicates)- Deprecated: Use
NORMAL_INinstead for full spatial relationship detail - Limited to containment only, cannot express expected positioning relationships
- Deprecated: Use
UI Dropzone System Mapping: The spatial dashboard maps Supabase predicates to visual dropzones:
- Left dropzone:
LEFT_OFrelationships - Right dropzone:
RIGHT_OFrelationships - Top dropzone:
ABOVErelationships - Bottom dropzone:
UNDERrelationships - Center dropzone:
INrelationships (containment)
Spatial Parent-Child Relationships
Defining Spatial Parents
A spatial parent is an object that directly contains or supports another object, creating a parent-child relationship where the child object follows the parent when moved.
Parent-Creating Relationships (child follows parent):
IN: Child is inside parent containerCONTAINS: Parent contains child (inverse of IN)ON: Child rests on parent support surface
Non-Parent (Positional) Relationships (objects remain independent):
ABOVE/UNDER: Vertical positioning without attachmentLEFT_OF/RIGHT_OF: Horizontal positioning without attachmentNEXT_TO: Adjacent positioning without attachmentATTACHED_TO: Physical attachment between equals (co-dependent)
Container Pattern with Parent Relationships
Objects can contain other objects through parent-creating relationships, creating spatial hierarchies:
// Parent-child chain (each arrow represents a parent relationship)
(:Keys)-[:IN]->(:Box)-[:ON]->(:Shelf)-[:IN]->(:Room)
// Movement propagation: Move Room → Shelf follows → Box follows → Keys follows
Warehouse Pallet Example
// Parent relationships (objects follow parent when moved)
(:Box1)-[:ON]->(:Pallet)
(:Box2)-[:ON]->(:Pallet)
(:Pallet)-[:IN]->(:WarehouseZone)
// Positional relationships (objects don't follow parent when moved)
(:Pallet2)-[:LEFT_OF]->(:Pallet)
(:Shelf)-[:ABOVE]->(:Pallet)
// Spatial hierarchy for Box1: [Box1, Pallet, WarehouseZone]
// Note: Pallet2 and Shelf are NOT in Box1's spatial hierarchy
Current vs Normal Location Model
The system distinguishes between:
- Current Location: Where the object currently is (using
CURRENT_*relationships) - Normal/Expected Location: Where the object should be (using
NORMAL_*relationships)
This enables powerful inventory auditing workflows that compare actual vs expected locations with full spatial relationship detail.
Misplacement Detection Examples
Same Container, Correct Placement:
(:Keys)-[:CURRENT_IN]->(:KeyDrawer)
(:Keys)-[:NORMAL_IN]->(:KeyDrawer)
// Status: ✅ Keys are in correct location
Same Container, Wrong Positioning:
(:Monitor)-[:CURRENT_LEFT_OF]->(:Computer)
(:Monitor)-[:NORMAL_RIGHT_OF]->(:Computer)
// Status: ⚠️ Monitor is on wrong side of computer
Wrong Container:
(:Box)-[:CURRENT_ON]->(:Pallet_A)
(:Box)-[:NORMAL_ON]->(:Pallet_B)
// Status: ❌ Box is on wrong pallet
Wrong Relationship Type:
(:Keys)-[:CURRENT_IN]->(:Drawer)
(:Keys)-[:NORMAL_ON]->(:KeyHook)
// Status: ❌ Keys are contained but should be hanging
GraphQL Field Definitions
The spatial system exposes several fields in the GraphQL API with specific semantic meanings:
Location Fields Reference
| Field | Type | Purpose | Data Source | Example |
|---|---|---|---|---|
location |
String |
Current location (human-readable) | spatialParent.name |
"Kitchen Drawer 2" |
spatialParent |
SpatialRef |
Direct container object | Neo4j (:obj)-[:IN]->(:container) |
{id: "123", name: "Kitchen Drawer 2", type: "Drawer"} |
spatialChildren |
[ObjectInstance] |
Objects contained in this object | Neo4j (:child)-[:IN]->(this) |
[{id: "456", name: "Fork", type: "Utensil"}] |
spatialHierarchy |
[SpatialRef] |
Path from object to root container | Neo4j parent relationship traversal | [{name: "Fork"}, {name: "Drawer"}, {name: "Kitchen"}] |
currentPlacement |
Placement |
Current location (structured) | Neo4j CURRENT_* relationships |
{id: "123", name: "Kitchen Drawer 2", type: "Drawer", relationship: "CURRENT_IN"} |
normalPlacement |
Placement |
Expected location (structured) | Neo4j NORMAL_* relationships |
{id: "789", name: "Utensil Rack", type: "Rack", relationship: "NORMAL_ON"} |
misplacementStatus |
MisplacementStatus |
Comparison of current vs normal | Computed from current vs normal relationships | {isCorrect: false, issue: "wrong_relationship", details: "Should be ON not IN"} |
positionalRelationships |
[PositionalRelationship] |
Relative positioning relationships | Neo4j [:LEFT_OF\|RIGHT_OF\|ABOVE\|UNDER\|ON\|ATTACHED_TO\|NEXT_TO] |
[{type: "LEFT_OF", relatedObject: {name: "Computer"}}] |
Field Usage Guidelines
For Display:
- Use
locationfor simple text display of current location - Use
spatialHierarchyfor breadcrumb navigation - Use
currentPlacement.nameandnormalPlacement.namefor location comparison
For Operations:
- Use
spatialParent.idfor spatial relationship operations (based onCURRENT_*parent relationships) - Use
currentPlacementvsnormalPlacementfor detailed misplacement detection - Use
misplacementStatusfor automated correction workflows - Use
spatialChildrenfor container contents listing
For Navigation:
- Use
spatialHierarchyto build location breadcrumbs (follows parent relationships only) - Use
spatialParent.idto navigate to parent container/support - Use
spatialChildren[].idto navigate to child objects - Important: Only parent relationships (
IN,CONTAINS,ON) are used for spatial hierarchy navigation
Implementation Details
Neo4j Relationships
// Current location (where object is now)
(:ObjectInstance {name: "Keys"})-[:CURRENT_IN]->(:ObjectInstance {name: "Kitchen Drawer"})
// Expected location (where object should be)
(:ObjectInstance {name: "Keys"})-[:NORMAL_ON]->(:ObjectInstance {name: "Key Hook"})
// Misplacement detection: Keys are IN drawer but should be ON hook
// Issue: Wrong relationship type and wrong target object
// Current positional relationships
(:ObjectInstance {name: "Lamp"})-[:CURRENT_LEFT_OF]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Mouse"})-[:CURRENT_RIGHT_OF]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Monitor"})-[:CURRENT_ABOVE]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Keyboard"})-[:CURRENT_UNDER]->(:ObjectInstance {name: "Monitor"})
// Normal/expected positional relationships
(:ObjectInstance {name: "Lamp"})-[:NORMAL_RIGHT_OF]->(:ObjectInstance {name: "Computer"})
(:ObjectInstance {name: "Mouse"})-[:NORMAL_LEFT_OF]->(:ObjectInstance {name: "Computer"})
// Result: Lamp and Mouse are on wrong sides - need to swap positions
// Current support and attachment relationships
(:ObjectInstance {name: "Laptop"})-[:CURRENT_ON]->(:ObjectInstance {name: "Desk"})
(:ObjectInstance {name: "Monitor"})-[:CURRENT_ATTACHED_TO]->(:ObjectInstance {name: "Arm"})
// Expected support relationships
(:ObjectInstance {name: "Laptop"})-[:NORMAL_IN]->(:ObjectInstance {name: "Storage_Case"})
// Result: Laptop should be stored, not on desk
// Current spatial hierarchy query (path to root via current parent relationships only)
MATCH path = (:ObjectInstance {id: $objectId})-[:CURRENT_IN|CURRENT_ON*0..]->(root:ObjectInstance)
WHERE NOT (root)-[:CURRENT_IN|CURRENT_ON]->() // root has no current spatial parent
RETURN [node IN nodes(path) | {id: node.id, name: node.name, type: COALESCE(node.class, 'GenericObject')}] as hierarchy
// Normal/expected spatial hierarchy query
MATCH path = (:ObjectInstance {id: $objectId})-[:NORMAL_IN|NORMAL_ON*0..]->(root:ObjectInstance)
WHERE NOT (root)-[:NORMAL_IN|NORMAL_ON]->() // root has no normal spatial parent
RETURN [node IN nodes(path) | {id: node.id, name: node.name, type: COALESCE(node.class, 'GenericObject')}] as hierarchy
// Misplacement detection query
MATCH (obj:ObjectInstance {id: $objectId})
OPTIONAL MATCH (obj)-[current:CURRENT_IN|CURRENT_ON|CURRENT_LEFT_OF|CURRENT_RIGHT_OF|CURRENT_ABOVE|CURRENT_UNDER|CURRENT_NEXT_TO|CURRENT_ATTACHED_TO]->(currentTarget:ObjectInstance)
OPTIONAL MATCH (obj)-[normal:NORMAL_IN|NORMAL_ON|NORMAL_LEFT_OF|NORMAL_RIGHT_OF|NORMAL_ABOVE|NORMAL_UNDER|NORMAL_NEXT_TO|NORMAL_ATTACHED_TO]->(normalTarget:ObjectInstance)
RETURN {
objectId: obj.id,
currentRelationship: type(current),
currentTarget: {id: currentTarget.id, name: currentTarget.name},
normalRelationship: type(normal),
normalTarget: {id: normalTarget.id, name: normalTarget.name},
isCorrectLocation: (type(current) = type(normal) AND currentTarget.id = normalTarget.id),
isCorrectRelationship: (type(current) = type(normal)),
isCorrectTarget: (currentTarget.id = normalTarget.id)
} as misplacementStatus
// Current positional relationships query
MATCH (obj:ObjectInstance {id: $objectId})
OPTIONAL MATCH (obj)-[r:CURRENT_LEFT_OF|CURRENT_RIGHT_OF|CURRENT_ABOVE|CURRENT_UNDER|CURRENT_ON|CURRENT_ATTACHED_TO|CURRENT_NEXT_TO]->(related:ObjectInstance)
RETURN {
relationshipType: type(r),
relatedObject: {id: related.id, name: related.name, type: COALESCE(related.class, 'GenericObject')},
properties: properties(r)
} as currentRelationship
// Normal/expected positional relationships query
MATCH (obj:ObjectInstance {id: $objectId})
OPTIONAL MATCH (obj)-[r:NORMAL_LEFT_OF|NORMAL_RIGHT_OF|NORMAL_ABOVE|NORMAL_UNDER|NORMAL_ON|NORMAL_ATTACHED_TO|NORMAL_NEXT_TO]->(related:ObjectInstance)
RETURN {
relationshipType: type(r),
relatedObject: {id: related.id, name: related.name, type: COALESCE(related.class, 'GenericObject')},
properties: properties(r)
} as normalRelationship
GraphQL Resolver Population
The getObjectDetailsComplete resolver populates spatial fields as follows:
# Current location fields (implemented)
"location": spatial_parent["name"] if spatial_parent else None,
"spatialParent": current_spatial_parent, # From current parent relationships (CURRENT_IN, CURRENT_ON, CURRENT_CONTAINS)
"currentPlacement": current_placement, # Current location with relationship type
"normalPlacement": normal_placement, # Expected location with relationship type
"spatialChildren": spatial_children, # Objects with current parent relationships to this object
"spatialHierarchy": current_spatial_hierarchy, # Current path to root container
"misplacementStatus": misplacement_status, # Comparison of current vs normal location
# Current relationship fields
"currentPositionalRelationships": current_positional_relationships, # Current CURRENT_* relationships
# Normal/expected relationship fields
"normalPositionalRelationships": normal_positional_relationships, # Expected NORMAL_* relationships
Resolver Enforcement & Upsert Logic (2025-07-01)
The GraphQL resolvers are the single point that guarantees spatial consistency.
-
moveObject(objectId, newContainerId, relationshipType)
– Deletes any existing current parent relationship from object ([:CURRENT_IN|CURRENT_ON]edges).
– Creates new current parent relationship ((:obj)-[:CURRENT_IN|CURRENT_ON]->(:new)based on relationshipType).
– Movement propagation: All child objects follow the moved object automatically.
– Misplacement tracking: Updates misplacement status after movement.
– Returns newcurrentPlacement,spatialHierarchy, andmisplacementStatus. - createOrMoveSpatialRelationship(objectId, targetId, relationship, locationType)
– Acts as an upsert for current or normal relationships based onlocationType(currentornormal).
– ForlocationType=current: UpdatesCURRENT_*relationships.
– ForlocationType=normal: UpdatesNORMAL_*relationships to set expected location.
– Ifrelationshipcreates parent relationship (CURRENT_IN,CURRENT_ON), delegate tomoveObject.
– For positional predicates:- Check
creates_parent_relationshipfield in spatial_predicates. - Delete any existing edge on that axis from the object.
- Insert the new edge using the predicate key with appropriate prefix.
– Misplacement update: Recalculates misplacement status after relationship changes.
– Predicate validation occurs against thespatial_predicatestable.
- Check
- Permissions
– Resolver checks abilitySPATIAL_MOVEfor current location changes. FutureSPATIAL_SET_EXPECTEDability will guard edits toSHOULD_BE_INrelationships.
By co-locating this logic in the backend we avoid race-conditions where a client would have to call “remove + add” and briefly leave invalid graph states. Clients simply express intent; the resolver mutates the graph safely and atomically.
Use Cases
UC-305: Inventory Audit & Misplacement Detection
Scenario: Compare where objects are vs where they should be
- System queries both
CURRENT_*andNORMAL_*relationships with full spatial detail - Dashboard compares
currentPlacementvsnormalPlacement - Flags mismatched objects for user attention
- User can either:
- Accept new location (update
SHOULD_BE_IN) - Move object to correct location
- Add to move collection for bulk operations
- Accept new location (update
Example:
// Object current location
(:Drill)-[:CURRENT_ON]->(:WorkBench)
// Object expected location
(:Drill)-[:NORMAL_IN]->(:ToolRack)
// Detailed issue: Drill is ON workbench but should be IN tool rack
// Result: Drill is misplaced, flag for user attention
Spatial Navigation
Breadcrumb Navigation: Use spatialHierarchy to show location path (parent relationships only)
- Display:
Workshop > Tool Cabinet > Drawer 3 > Screwdriver Set - Each level is clickable to navigate to that parent container/support
- Important: Only follows parent relationships (
IN,CONTAINS,ON)
Container/Support Contents: Use spatialChildren to show what’s contained or supported
- Display contents of selected container or objects on support surface
- Support drill-down navigation through spatial hierarchy
- Example: Select pallet → shows boxes ON the pallet, not pallets LEFT_OF it
Positional Relationship Display: Show adjacent/positioned objects separately
- Display objects with positional relationships as “nearby” or “adjacent”
- Don’t include in spatial hierarchy or movement operations
- Example: Show “Pallets nearby” section for LEFT_OF/RIGHT_OF relationships
Related Documentation
- Relationship Decision Guide - When to use spatial vs functional relationships
- Object Status System - How location relates to object status
- API Endpoints - GraphQL schema for spatial fields
Implementation Status
✅ Implemented
- Basic
INrelationship queries spatialParent,spatialChildren,spatialHierarchypopulationlocationfield as current location text- Spatial breadcrumb components
- 🆕 Hierarchical Spatial Dashboard - Tree view showing objects in real-world spatial structure
- 🆕 5-Zone Dropzone System - Visual drag-and-drop spatial relationship creation with global state management
- 🆕 Positional relationships -
TO_LEFT,TO_RIGHT,ABOVE,BELOWvia GraphQL mutations and queries - 🆕 Nested Card Layout - Visual representation of containment relationships
- 🆕 GraphQL mutations -
createSpatialRelationship,removeSpatialRelationship,moveObject - 🆕 Enhanced spatial queries -
GET_SPATIAL_DASHBOARD_DATAwith full relationship data,myObjectsnow includes spatial hierarchy
❌ TODO: Missing Implementation
- Dual predicate system migration - Migrate from legacy predicates to
CURRENT_*/NORMAL_*system misplacementStatusfield implementation - Real-time comparison of current vs normal location- Normal location management UI - Interface for setting expected locations with full spatial detail
- Bulk misplacement correction - Workflows for correcting multiple misplaced objects
- Advanced misplacement analytics - Reports on misplacement patterns and trends
- Parent relationship movement logic - Implement movement propagation for parent-child relationships using current predicates
- Spatial hierarchy traversal update - Modify queries to use current parent relationships only
- Dynamic spatial predicate loading - UI should load available current/normal predicates from Supabase
- Predicate validation - GraphQL mutations should validate against spatial_predicates table with dual prefixes
- Legacy predicate deprecation - Phase out unprefixed predicates and SHOULD_BE_IN
- Misplacement notification system - Alerts when objects move away from normal locations
For implementation details, see the System Architecture and Brand Requirements for UI specifications.