Partition Storage Routing

A partition is a unit of physical storage isolation. On Postgres it is a schema ({partition}.mesh_nodes + satellite tables). Partitions are NOT a pervasive abstraction the rest of the system reasons about — they matter for exactly two things, and everything else is derived from NodeType configuration.

Partitions matter for two things

1. Queries — fan to every adapter; absent partition → empty

A query is sent to every storage adapter. There is no fan-out decisioning in the query provider (no "which schemas, pin-vs-fan-out, satellite-vs-mesh_nodes" branching). Each adapter answers for its own data; if the partition it would read doesn't exist, it returns an empty result (Postgres 42P01 → empty), never an error and never a slow tree walk. The union of the adapters' answers is the query result.

2. Mapping an object to its storage adapter — longest-prefix-match wins

To route a single object (read/write/create) to its owning storage adapter, there is no registry and no NodeType→schema map. Routing is purely data-driven:

Ask every storage adapter for its longest stored path P that is a prefix of the target path (the target starts with P), ordered by length descending. Across all adapters, the adapter with the maximum matching-prefix length wins — it owns the target path and persists/reads it. (This is IStorageAdapter.FindBestPrefixMatch fanned across every adapter, picking the max.)

The winning adapter is simply the one already holding the closest ancestor of the path. A partition root (rbuergi) is held by that partition's adapter, so rbuergi/_UserActivity/x longest-prefix-matches it and routes there. If NO adapter has a matching prefix, nothing owns the path — a read returns empty and a write is refused (unless it is a partition-owning create, below).

The create/read logic layered on top:

Invariant: it is always clear who saves where. Schema + table are a deterministic function of the object's path (+ NodeType for the fallback) and its top-level partition. If the partition does not exist, the write is refused — the storage layer never conjures a schema for an unrecognised path segment. (This is the root-cause fix for the schema-corruption where any path segment — NodeType names, reserved words, request URLs — spawned a ghost schema.)

NodeType configuration (the source of truth)

Each NodeType declares its storage shape once, on its NodeType definition (its NodeTypeDefinition content, set in the type's builder — e.g. SpaceNodeType, UserNodeType, UserActivityNodeType). This is implicit, type-level info — NOT a per-instance MeshNode property and NOT a hard-coded central dictionary or temporary registry.

On create, the NodeType definition is loaded and consulted directly. A CreateNodeRequest ships the node (including its NodeType); the create path loads that NodeType's definition and reads off it whether the type owns a partition / which table it persists to / etc. There is nothing to look up in a side registry — the NodeType definition node is the single source, read on demand. (Routing of existing objects, by contrast, needs none of this — it is the longest-prefix-match in §2.)

NodeType Configured as Result
Space owns a partition top-level Space → its own schema; creator becomes Admin
User owns a partition top-level User → its own schema; creator becomes Admin
UserActivity owns a table stored in its own satellite table inside the owning partition's schema
Thread / ThreadMessage owns a table satellite table (threads)
AccessAssignment owns a table satellite table (access)
(default) the partition's primary mesh_nodes table

The hard-coded static PartitionDefinition.StandardTableMappings / NodeTypeToSuffix dictionaries are already gone — the defaults now come from the configurable SatelliteTableMapping.Defaults, surfaced as PartitionDefinition.DefaultSegmentTableMappings() (segment → table) and DefaultNodeTypeTableMappings() (nodeType → table). What remains is the _Thread/_Access/… path-suffix matching being the primary router input: the goal is for a node's storage table to come from its NodeType's configuration rather than the shape of its path, so adding a new satellite type is a one-line configuration on that NodeType — no central map to edit, no router branch to add.

The only framework partitions: public, admin, auth

Beyond per-User/Space partitions, the clean model keeps exactly three system schemas, all created eagerly by the migration (never lazily, never by an app write):

Schema Purpose Who writes
public shared tables + central main-node index + the ensure_partition_schema stored proc migration
admin version tracking + global catalogs (agents / models / roles) system, via normal persistence
auth access-object lookup mirror (User/Group/Role/VUser/ApiToken/Space rows) trigger only — application code NEVER writes to auth (PartitionWriteGuardValidator rule 1 blocks it). The V27 mirror_access_object_to_auth_schema trigger populates it; the schema is provisioned at portal boot by PostgreSqlPartitionSubscriptionHostedService (it provisions every registered framework partition), so the trigger has a destination before any user write. NOT created by the migration's SchemaInitialization — doing so made auth.mesh_nodes exist before the fresh-DB check, which mis-classified a fresh DB as non-fresh and ran the legacy user-schema repair chain (V05+).

Legacy partitions are gone (full cut). Portal / Kernel session partitions are removed — compilation / script execution is an Activity in the owning partition's activities table, not a kernel schema (the standalone kernel/* address was retired; the kernel runs inside the Activity MeshNode hub). The global _Access / _Activity / _UserActivity / _Thread satellite partitions and their global AccessAssignments are removed too: per-partition _Access holds grants, and the system identity gets Permission.All from the PermissionEvaluator fast-path (no data-model grant). DefaultPartitionProvider now seeds only Admin + Auth.

Content partitions materialized from a static repoDoc (embedded documentation), sample graphs, seed data — are populated into their partition (content + prerender, served from the DB, not an in-memory overlay) by the Static-Repo Import pattern: a content-addressed Activity, idempotent via a source fingerprint, run once per content-version. The authored files on disk are the source; the partition rows are the serving copy.

Implementation status (2026-06-05)

Done (the ghost-schema corruption fix):

Pedestrian query-provider retirement (partial, done):

Still design / migration debt (the broader query redesign, tracked separately):

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.