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 connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.