ObjectStackObjectStack

External Datasources (Federation)

Declare an external database as a datasource and query its tables as ObjectStack objects — visible, auto-connected, and queryable with zero app code.

External Datasources (Federation)

ObjectStack can treat a mature external database — one it does not own — as a read-only (or, with explicit opt-in, writable) datasource, and expose its tables as normal objects. This is federation: the data stays in the remote database; ObjectStack queries it live through the same ObjectQL / REST surface as native objects.

The headline guarantee: declare an external datasource and it is visible, auto-connected, validated at boot, and queryable — with no application code.

This guide is about reading/writing the live remote tables (federation). To copy rows into ObjectStack-owned tables instead, see seed data / data sync. For the plain multi-datasource routing of managed databases ObjectStack owns, see Database Drivers.

1. Declare the datasource

Use defineDatasource with schemaMode: 'external'. The external block carries the federation policy (write gate, boot validation, credentials).

import { defineDatasource } from '@objectstack/spec/data';

export const Warehouse = defineDatasource({
  name: 'warehouse',
  label: 'Analytics Warehouse (Postgres)',
  driver: 'postgres',
  schemaMode: 'external',          // ObjectStack never runs DDL here
  config: { host: 'db.internal', port: 5432, database: 'analytics', user: 'readonly' },
  external: {
    allowWrites: false,            // read-only (the default)
    credentialsRef: 'secret:warehouse/password', // resolved from the secret store
    validation: { onMismatch: 'fail', checkOnBoot: true },
  },
  active: true,
});

Register it on the stack (array or name-keyed map both work):

export default defineStack({
  datasources: [Warehouse],
  // ...
});

schemaMode:

ModeMeaning
managed (default)ObjectStack owns the schema — DDL + migrations allowed.
externalA mature external DB — DDL forbidden; a schema mismatch fails boot.
validate-onlyLike external, but a mismatch warns instead of failing.

2. Bind objects to the remote tables

A federated object sets datasource to the external datasource and declares its remote binding in external. When the remote table or column names differ from your object/field names, map them with external.remoteName / external.remoteSchema and external.columnMap.

export const Customer = ObjectSchema.create({
  name: 'ext_customer',
  datasource: 'warehouse',
  external: {
    remoteName: 'customers',       // remote TABLE name (object name may differ)
    // remoteSchema: 'public',     // optional schema/namespace (pg/mysql)
    // columnMap: { cust_region: 'region' }, // remoteColumn → localField
  },
  fields: {
    id: { type: 'text' },
    name: { type: 'text' },
    region: { type: 'text' },
  },
});

That's it. GET /api/v1/data/ext_customer now returns live rows from the remote customers table; filters (?region=EU) push down to the remote query.

Do not set field.columnName on an external object. For federated objects the driver's query pipeline ignores field.columnName; external.columnMap (remoteColumn → localField) is the single, authoritative column mapping. os build / os validate rejects field.columnName on an external object with a clear error (ADR-0062 D7). Managed objects are unaffected.

3. Auto-connect — no onEnable needed

At boot the runtime builds a live driver for the datasource, connects it, and registers its federated objects' read metadata — automatically. You do not need an onEnable hook or ctx.drivers.register(...).

A declared datasource auto-connects when it is meaningfully addressed:

  1. it is external (schemaMode !== 'managed'), or
  2. an object explicitly binds to it via object.datasource === <name>, or
  3. it sets autoConnect: true.

A managed datasource that nothing explicitly binds to (for example one that is only referenced by a datasourceMapping rule) stays metadata-only — visible in Setup, but not connected — so existing apps are unchanged. Use autoConnect: true to opt such a datasource into a live connection at boot.

Escape hatch. An onEnable hook calling ctx.drivers.register(driver) is still supported for advanced cases — e.g. a driver built dynamically at runtime from external configuration. Auto-connect is idempotent with it (whichever registers the datasource name first wins), so the two never conflict.

A connected external datasource is also visible in Setup → Datasources (stamped origin: code, read-only in the UI) and via GET /api/v1/datasources and GET /api/v1/meta/datasource, where an admin can run the "Sync objects" wizard.

4. Credentials

Never inline a password. Put a reference in external.credentialsRef and store the secret in the secret store (the same SecretBinder / ICryptoProvider the runtime-admin "Add Datasource" wizard uses). The credential is resolved to cleartext at connect, before the driver is built.

Resolution is fail-closed: if a credentialsRef is declared but no secret store is configured, or the secret cannot be resolved/decrypted, the datasource is left unconnected with a clear error — never connected without the credential. For a code-defined external datasource with validation.onMismatch: 'fail' this fails the boot; otherwise it degrades with a warning. (Credential-less drivers such as SQLite simply have no credentialsRef.)

5. Writes (double opt-in)

Federation is read-only by default. To allow writes, both the datasource and the object must opt in:

defineDatasource({ /* ... */ external: { allowWrites: true } });   // datasource gate
ObjectSchema.create({ /* ... */ external: { remoteName: 'orders', writable: true } }); // object gate

With either gate off, insert/update/delete on the federated object is rejected.

6. Analytics over external objects

Dashboards and reports over a federated object aggregate against the correct remote table/columns: the analytics layer routes such queries through the driver's physical-table resolution (honoring remoteName / remoteSchema) rather than the object name, so they never hit the wrong table.

Multi-tenant hosts

A self-hosted single-environment runtime connects external datasources out of the box. A multi-tenant host can bind a stricter connect policy (egress allow-list / per-tenant quota) that is consulted before any connection is opened — the same single connect path, no fork. This is a host-composition concern; the open-core default allows all connects (subject to the gating above).

See also

  • Database Drivers — managed multi-datasource routing.
  • Datasource reference — every defineDatasource field.
  • The examples/app-showcase showcase_external datasource — a runnable end-to-end demo.
  • ADR-0015 (federation spec) and ADR-0062 (external-datasource runtime).

On this page