Storage Bindings — Where a Partition Keeps Its Data

Nobody has to configure anything. With zero bindings, every purpose resolves to what the instance does today: its own PostgreSQL, in the partition's own schema; its own content storage. A storage binding only overrides — one purpose, of one partition — and only once it has been validated.

The default flow is select-or-create inside the pre-configured storage: the instance discovers its own stores from its configuration and identity, lists the containers that exist in them, and a binding either picks one of those or creates a new one there with the instance's own identity. A completely separate account is the advanced option and is recorded as a vault reference only.

Purposes

StoragePurpose is an open vocabulary of string constants (policy open-vocabulary-string-constants), spelled as an enum would be:

Purpose What it stores Built-in default
DurableStream durable stream state (queues, checkpoints) the instance database — instance-wide, so a partition without its own binding falls back to Admin's
DocParts the parts a document is cut into the instance database, the partition's schema
Originals the original files of a content collection the instance's content storage
VectorIndex the vector index (content_chunks) the instance database, the partition's schema

A module may bind a purpose of its own in the same field; an unknown purpose simply has no binding and resolves to the default.

Where a binding lives

A binding lives in the partition whose storage it configures, in that partition's own storage table:

Scope Path Who administers it
a partition (a Space, a person, a package) {partition}/_Storage/{id} whoever holds Update on the partition root, by ordinary grants
the instance (global) Admin/_Storage/{id} a platform admin (Update on Admin)

_Storage is a satellite segment (SatelliteTableMapping.Defaults → table storage, node type StorageBinding), placed directly under the partition root — never nested under another satellite. The storage table is created by public.ensure_partition_schema like every other satellite table: a new partition gets it at provisioning, an existing one on the next migration pass or the first touch of a fresh pod.

The node

StorageBinding (MeshWeaver.Mesh.Storage):

Field Meaning
Purpose what it is for
StoreId the pre-configured store; empty = the store the instance uses by default for the purpose
Container the container in it — a Postgres schema, a blob container, a directory
TablePrefix a prefix for the tables the purpose creates, where the store has tables
Collection the content collection it serves; empty = the whole partition
IsDefault the partition's default binding for the purpose
NewContainerName input to Create
VaultSecretName / ManagedIdentityClientId / ExternalEndpoint ADVANCED — a separate account, as a vault reference. Never a secret.
RequestedAction, RequestedAt what the viewer asked the binding's hub to do (Validate, Create)
ValidationStatus, ValidationMessage, ValidatedAt, ValidatedTarget the verdict, recorded on the node by its own hub

A sample binding, in the partition fabrikam:

{
  "id": "parts",
  "namespace": "fabrikam/_Storage",
  "nodeType": "StorageBinding",
  "content": {
    "$type": "StorageBinding",
    "purpose": "DocParts",
    "storeId": "postgres",
    "container": "fabrikam_parts",
    "isDefault": true,
    "validationStatus": "Valid",
    "validationMessage": "'fabrikam_parts' exists in 'Instance database (PostgreSQL · <database>)' and is reachable."
  }
}

Pre-configured stores

An IInstanceStore is one of the instance's own stores, registered in DI by the module that owns the connection. It lists its containers live, creates one idempotently with the instance's identity, and probes one. Every call goes through IIoPool.

Store Kind Registered by Default for
postgres — the mesh's database PostgresSchema MeshWeaver.Hosting.PostgreSql (partitioned persistence) DocParts, VectorIndex, DurableStream
content — the content storage root, {Storage:BasePath}/content Directory Memex.Portal.Shared when Storage:SourceType is FileSystem Originals
blob — the content-storage blob account BlobContainer Memex.Portal.Distributed on the Azure backend Originals when Storage:SourceType is AzureBlob

A store's DefaultContainerFor(partition) is exactly what the instance uses today (Postgres: lower(partition), the partition's schema).

Which containers a partition may bind

The stores are shared: every partition's schema sits in the same database. A partition that could bind any existing schema could bind another partition's — and read or overwrite its data. So (StorageContainerOwnership):

The resolver re-applies the WHOLE rule — the prefix and "not another partition's default", against the partition catalog read live — on every answer, before it honours a binding. So neither a verdict written onto a node by hand nor a partition created after the binding was validated can route one partition's data into another's container; with the catalog unreadable, only the partition's own default container is trusted. Admin is matched case-sensitively, like the path its bindings live under.

Validation — on every save

The binding's own hub runs a watcher (StorageBindingValidation). Whenever the target changes (store, container, prefix, separate account — every field TargetKey() covers) or a RequestedAction arrives, it checks, in order: separate account (recorded as Unverified — this instance has no connector for separate accounts, so such a binding never overrides), the store exists, the name passes the store's own rule, the partition may use it, it is not another partition's default — and only then contacts the store (Probe, or EnsureContainer for Create). The verdict is written onto the node as System, about the target it was decided on; an edit that landed meanwhile is validated in its own turn. The trigger is a pure function of the content that is null once the verdict for the current target is recorded, so the watcher never feeds on its own writes.

Status Meaning
Pending not validated yet, or the target moved
Valid reachable, the container exists, the instance's identity may write to it — the only status that overrides
Invalid a malformed name, a container the partition may not use, a store that does not exist, a container that does not exist
Unreachable the store did not answer, or refused the instance's identity
Unverified a separate account — recorded, never used

The resolver — what consumers call

public interface IStorageBindingResolver
{
    IObservable<ResolvedStorage> Resolve(string purpose, string partition, string? collection = null);
}

public sealed record ResolvedStorage(
    string Purpose, string Partition, string StoreId, string StoreKind, string Container)
{
    public string? TablePrefix { get; init; }
    public string? BindingPath { get; init; }   // null ⇒ the built-in default
    public bool IsDefault => BindingPath is null;
    public string? VaultReference { get; init; }
}

The answer is live — emitted at once and again whenever a deciding binding changes, de-duplicated; take the first value when you need it once. Order: the partition's own usable binding (one naming collection before its default one) → for an instance-wide purpose, Admin's → the built-in default. IsDefault means do exactly what you did before bindings existed; only a non-default answer needs rewiring. The resolver reads the binding nodes as the infrastructure identity — storage routing never depends on who triggers it.

The settings surfaces

ONE content builder, two surfaces, both framework controls bound to the binding nodes:

Each section is a template of framework controls: the pre-configured stores as a DataGrid (store, kind, what it is the default for, this partition's default container, how many containers this partition may bind — read live), an Add binding button, and the partition's bindings as a MeshSearch list the viewer's client runs under the viewer's own access. Each binding opens its OWN page (StorageBindingLayoutArea, the node's default and Edit area): the recorded verdict as read-only fields of the node, a MeshNodeContentEditorControl bound directly to the node — purpose, store and container are dropdowns over the instance's stores and the containers this partition may bind in them, read live, so a container created a moment ago is offered at once — and Create container / Validate buttons that write RequestedAction on the node. The node's hub acts and records the verdict; the page re-renders from the node. No /data replica, no save loop; deleting a binding is the node's own Delete.

What is not here yet

See also: Settings by Owner · PostgreSQL schema architecture · Satellite node patterns.