World root data model — single shared root, default-to-World, backfill

Created: Wed 24 Jun 2026 10:09:24 CEST Updated: Wed 24 Jun 2026 10:09:24 CEST - Initial design Document Version: 1.0 - Initial design Security Classification: Internal Technical Documentation Target Audience: Backend Engineer, iOS Engineer, Architect Author: Paul Wisén

Scope: cross-repo (Plings-API + Neo4j migration, Plings-iOS). System-spanning, so this spec lives in the Plings-Web docs hub. Each executing repo gets its own implementation plan. This is sub-project #1 of a three-part vision (see “Out of scope” / “Future” below).

Problem

Today “root” means “no relationship”: a top-level ObjectInstance has zero spatial edges in Neo4j, and “is this a root?” is answered by the absence of a parent. This is elegant in the abstract but leaks everywhere:

Vision context (why a real root, not a patch)

Plings is a world-spanning system — a global knowledge graph of physical objects. If everything physical lives in the world, then “an object with no location” should not be representable at all. The root of containment is a real thing: the World. The model must be depth-agnostic — the day Plings reaches the Moon we add “Universe” above World with World/Moon beneath it; later we may insert continents → countries → municipalities between World and a property. The graph simply shows the highest objects (that you can see) which have sub-objects. Navigation at scale is a map (pan/zoom-to-enter), not breadcrumb drill-down — but that is presentation; this spec is the data model underneath it.

ADR-0039 anticipated this: “a globe fits the world-spanning vision and survives the future scope-root idea.”

Decision

Introduce a single shared World root ObjectInstance. Every object always has a real parent chain ending at World; nil survives only as World’s own parent — the one universal base case. This replaces the implicit “root = no relation” model.

1. The World node

2. Default-to-World + backfill (the two load-bearing requirements)

3. iOS — root becomes World, the drag bug disappears

4. Depth-agnostic guarantees + what is explicitly OUT

Rollout / merge order

Cross-repo, so deploy backend first:

  1. Plings-API — World node creation + backfill migration + createObject default-to-World + access special-case + delete/move guards. Run the migration against Neo4j Aura + Supabase.
  2. Plings-iOSWORLD_ID constant, navigator root = World, drag-to-globe, delete the root-drop special-case.

The deployed app must never re-home to World before the backend can accept NORMAL_IN World, so iOS ships after the API.

Testing

Plings-API (pytest) + migration:

Plings-iOS (swift test + device):

ADR

A new ADR records this: the World is a real shared root; nil-as-root is retired. It supersedes the implicit no-relation-root model and refines ADR-0039 (globe root), ADR-0005 (move writes CURRENT), ADR-0020 (navigator reads NORMAL tree), and ADR-0044 (navigator drag is a home-map op — the root-drop carve-out it noted is now removed). Ships with the implementation (code-coupled; the API PR is the natural home, cross-referenced from iOS).

Open questions / implementation notes