Skip to content

Multi-Instance and Instance Resolution

This guide explains how multi-instance behavior works in Vona within the Cabloy monorepo.

Why this matters

In Vona, backend runtime behavior is not always global-only.

A backend application may need:

  • a shared default instance
  • named instances for tenant-like separation
  • isolated instances with their own datasource routing
  • instance-aware config, startup, and model behavior

That is why multi-instance behavior should be understood as part of the backend runtime/config family, not only as an ORM detail.

Three practical layers

A useful mental model is:

  • runtime and config decide how instances are declared
  • request context decides which instance is active
  • model and datasource behavior decide how that instance affects persistence

Instance config shape

In the current runtime, instance behavior is configured under config.instance.

Representative shape:

typescript
config.instance = {
  getInstanceName: undefined,
  queryField: $protocolKey('x-vona-instance-name'),
  headerField: $protocolKey('x-vona-instance-name'),
  instances: {
    '': { password: '', title: '' },
    'shareTest': { password: '', title: '' },
    'isolateTest': {
      password: '',
      title: '',
      id: 1000,
      isolate: true,
      isolateClient: 'isolateTest',
    },
  },
};

The important fields are:

  • getInstanceName
  • queryField
  • headerField
  • instances
  • per-instance options such as id, isolate, isolateClient, and config

Shared instance vs isolated instance

A practical distinction is:

  • a shared instance participates in the normal shared datasource strategy
  • an isolated instance can route to its own datasource through isolateClient

This means multi-instance behavior is not only a naming concern. It can materially change which database path the backend uses.

How the active instance name is resolved

In the current runtime, the instance name is resolved in this order:

  1. getInstanceName(ctx) if provided
  2. the configured query field
  3. the configured header field
  4. subdomain-derived instance name

This matters because request routing, config, and datasource behavior may all depend on the resolved instance name.

A special case is also supported:

  • 'null' is normalized to null

A practical fallback rule is:

  • subdomain-based instance resolution only becomes relevant if custom getter, query-field, and header-field resolution did not already determine the instance name

Subdomain mapping and the default instance

SERVER_SUBDOMAINOFFSET controls how many labels at the right side of a hostname are treated as the base domain. Its default value is 2, which fits a two-label base domain such as cabloy.test or example.com.

With that default, Vona derives the instance name from the remaining labels:

HostnameDerived instance name
cabloy.test''
acme.cabloy.testacme
eu.acme.cabloy.testacme.eu

The last example is intentionally acme.eu: Koa reverses the remaining hostname labels before Vona joins ctx.subdomains with ..

The empty name '' is the configured default instance. It is not an implicit fallback for every hostname: it can be disabled or removed. Each non-empty subdomain-derived name, including a deeper name such as acme.eu, must refer to an explicitly configured and enabled instance. Requests for an unknown, disabled, or deleted instance do not fall back to the default instance.

If the public base domain uses a different label shape, set an explicit SERVER_SUBDOMAINOFFSET override that matches that domain before relying on subdomain instance resolution.

Instance edge cases to remember

A few current-runtime edge cases are worth remembering:

  • disabled instances can be declared with false
  • isolated instances require an explicit id
  • instance-name resolution is cached on the current context once determined
  • the instance record can be created or refreshed through the instance service before the effective config is cached

What context exposes

Once instance resolution is active, the request context can expose:

  • ctx.instanceName
  • ctx.instance
  • ctx.config

A practical distinction is:

  • ctx.instanceName identifies the active instance
  • ctx.instance is the loaded instance record
  • ctx.config is the effective config merged for that instance-aware context

That means request-scoped backend code can stay instance-aware without manually reassembling config or routing rules.

How instance config affects effective backend config

This page owns the request-context and instance-aware merge view. For env-file precedence and mode/flavor selection, see Runtime and Flavors. For the broader config-layering surface, see Config Guide.

In the current runtime, instance-aware config is built by merging:

  1. app.config
  2. static config under config.instance.instances[instanceName].config
  3. any persisted config stored on the instance record

This is why instance behavior belongs partly to config docs and partly to runtime docs.

A practical merge-order mental model is:

  1. start from app.config
  2. merge static instance config from config.instance.instances[instanceName].config
  3. merge persisted config stored on the instance record
  4. expose the result through ctx.config

For the broader config-layering story, also see Config Guide.

How instance affects startup behavior

Startup is also instance-aware.

A practical split is:

  • app startup handles backend-wide initialization
  • instance startup handles per-instance initialization after the app is ready for instances

In the current runtime:

  • test and dev eagerly start the default instance
  • production-style flows iterate configured static instances
  • instance startup work is queued and coordinated so one instance can initialize cleanly

For the lifecycle view, also see Backend Startup Guide.

How instance affects datasource selection

One of the most important multi-instance consequences is datasource routing.

In the current runtime, datasource selection can work like this:

  1. use an explicit datasource when code names one
  2. otherwise use the configured default datasource
  3. if the active instance is isolated, let isolateClient override that default

This is why instance isolation should be understood together with datasource strategy rather than documented as an unrelated feature.

For the datasource view, also see Multi-Database and Datasource Guide.

How instance affects model behavior

Ordinary model operations are instance-aware by default.

That means:

  • normal model flows participate in instance filtering
  • instance-aware config and startup can influence model behavior indirectly
  • disableInstance changes semantics and should be used deliberately

For the model-layer perspective, also see Model Guide.

Tenancy boundary: one Vona tenant per instance

In Vona's current tenancy model, a tenant corresponds to an instance. The resolved active instance is the data-isolation boundary for ordinary resource code.

For resources that inherit the normal entity base behavior:

  • iid persists the instance discriminator when instances share a datasource
  • normal model CRUD uses the active ctx.instance.id rather than a caller-supplied tenant or iid
  • isolated instances can add a separate datasource boundary through isolateClient, but they still remain separate instances

This means a normal scoped lookup has one business result when it finds no row: that resource is absent in the current tenant/instance. Do not bypass model scope or issue an unscoped secondary lookup merely to reveal that another instance has the same identifier and change the result to 403.

This rule does not eliminate genuine authorization failures. Return 403 when the current request is not authorized to perform an operation on a resource that is available in its authorized scope. The rule only prevents ordinary scoped misses from becoming cross-instance existence disclosures.

Do not use instance selection to represent a future merchant, store, organization, or similar business boundary. A multi-merchant design belongs inside one instance and needs explicit merchant ownership, authorization, relations, indexes, and tests in addition to the existing instance boundary.

For the operational model rules, see Model Guide. For generated resource behavior, see CRUD Workflow.

Relationship to runtime and config docs

Read this guide together with:

Implementation checks for instance-resolution changes

When editing backend runtime, datasource, or tenant-like behavior, ask:

  1. how is the current instance name resolved?
  2. is the task about shared-instance behavior or isolated-instance behavior?
  3. should the logic read app.config or ctx.config?
  4. does instance startup or instance config need to change as well?
  5. does datasource routing change indirectly through isolateClient even if no model call names a client explicitly?

That keeps multi-instance behavior aligned with the current Vona runtime rather than treating it as a loose convention.

Released under the MIT License.