Configure

Data Sources

Connect ObjectOS to your existing business databases, route objects to them, and let AI query the data — natively.

A datasource is a named connection to an external data store. By declaring datasources, you point ObjectOS at the databases your business already runs on — a production PostgreSQL, a reporting MySQL replica, a MongoDB cluster — and then bind objects to them. Once an object is bound, everything else in the platform (the REST API, permissions, flows, dashboards, and AI agents) works against that data uniformly, without caring where the rows physically live.

This is one of ObjectOS's most practical adoption paths: instead of migrating a legacy system, you connect to it, model the tables you care about as objects, and extend it with AI-native capabilities — chat, analysis, automation — on top of data that stays exactly where it is.

What a datasource is

Each datasource is a plain object validated by DatasourceSchema. The core fields:

FieldPurpose
nameUnique identifier (^[a-z_][a-z0-9_]*$) objects reference
labelHuman-readable display name
driverWhich driver handles the connection (postgres, mysql, sqlite, mongodb, memory, or a plugin-contributed driver)
configDriver-specific connection settings (host, database, credentials, …)
poolConnection-pool sizing (min, max, timeouts)
schemaModeWho owns the schema: managed (the default) lets ObjectOS create and alter tables; external or validate-only declares a database ObjectOS does not own, where it runs no DDL
externalThe federation policy of an external datasource — its write gate and boot validation — plus credentialsRef, the secrets-store reference any datasource may carry
activeWhether the connection is enabled

readReplicas, capabilities (with its readOnly flag), healthCheck and retryPolicy were removed in ObjectStack 17.0: no runtime path read any of them. The parse refuses each one, naming the mechanism that does decide the behaviour.

The shipped drivers are:

Driver packagedriverBackends
@objectstack/driver-sqlpostgres, mysql, sqlitePostgreSQL, MySQL, SQLite (via knex)
@objectstack/driver-mongodbmongodbMongoDB
@objectstack/driver-memorymemoryIn-process, for tests — not a boot store since ObjectStack 17.7
@objectstack/driver-sqlite-wasmsqlite (WASM)SQLite in WebContainers / browser

Declaring datasources

Datasources are declared on the stack and assembled with defineStack. Define each connection with defineDatasource(), then list them under datasources:

// src/datasources/business.datasource.ts
import { defineDatasource } from '@objectstack/spec';

// Connect to an EXISTING production database that ObjectOS does not own.
// `external` mode: ObjectOS runs no DDL here, and writes stay off until you
// opt in. The password is never inline: it lives in the secrets store, and
// the datasource only references it.
export const BusinessDb = defineDatasource({
  name: 'business_primary',
  label: 'Business System (Postgres)',
  driver: 'postgres',
  schemaMode: 'external',
  config: {
    host: 'db.internal.example.com',
    port: 5432,
    database: 'business',
    username: 'objectos_readonly', // a database user granted SELECT only
  },
  external: {
    allowWrites: false, // the default: the datasource refuses writes
    credentialsRef: 'secret:business_db/password',
  },
  pool: { min: 1, max: 10 },
  active: true,
});
// objectstack.config.ts
import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects/index.js';
import { BusinessDb } from './src/datasources/business.datasource.js';

export default defineStack({
  manifest: {
    id: 'app.example.crm-extend',
    namespace: 'biz',
    version: '1.0.0',
    type: 'app',
    name: 'CRM extension',
  },
  datasources: [BusinessDb],
  objects: Object.values(objects),
});

defineDatasource() validates where you write it. Exported from @objectstack/spec (and @objectstack/spec/data), it runs the DatasourceSchema validation at the point of declaration, so a bad field is reported in the file that has it. The framework's examples/app-crm stack declares its datasources this way. A plain object typed Datasource (from @objectstack/spec/data) is also accepted, and is validated when the stack is assembled.

Credentials are never inline. DatasourceSchema refuses a password in config, even one read from the environment: the datasource is stored whole in sys_metadata, so an inline secret would sit there in cleartext. Put the secret in the secrets store and name it in external.credentialsRef. The resolved secret is injected at connect time.

Binding objects to a datasource

Every object has a datasource field. It defaults to 'default' (the primary database). Set it to route a specific object at one of your connected systems:

import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Customer = ObjectSchema.create({
  name: 'biz_customer',
  label: 'Customer',
  sharingModel: 'private',
  datasource: 'business_primary', // ← queries go to the business DB
  external: { remoteName: 'customers' }, // the remote TABLE name
  fields: {
    name:  Field.text({ label: 'Name', required: true }),
    email: Field.text({ label: 'Email' }),
    tier:  Field.select({
      label: 'Tier',
      options: [
        { label: 'Standard', value: 'standard' },
        { label: 'Premium', value: 'premium' },
      ],
    }),
  },
});

A datasource that objects bind to must connect, or the boot fails. Since ObjectStack 17.0 an unreachable datasource is a startup failure, not a warning line. Previously only an external datasource with validation.onMismatch: 'fail' fail-fasted: an app declaring datasource: 'analytics' with objects bound to it, booted against a wrong URL, started clean and exited zero — then failed every read and write of those objects with Datasource 'x' is not registered. /api/v1/ready also reports 503 when a data driver stops answering, so a replica whose database has gone away is taken out of rotation instead of serving errors. Connection attempts are bounded at 10 seconds. See the 17.0 release notes.

Centralized routing with datasourceMapping

Binding objects one by one is fine for a handful. For whole namespaces or packages, declare routing rules once on the stack. Rules are evaluated in order (or by priority); first match wins:

// A second connection, for reporting.
export const AnalyticsReplica = defineDatasource({
  name: 'analytics_replica',
  label: 'Analytics replica',
  driver: 'postgres',
  schemaMode: 'external',
  config: {
    host: 'replica.internal.example.com',
    port: 5432,
    database: 'business',
    username: 'objectos_reports',
  },
  external: { credentialsRef: 'secret:analytics_db/password' },
});

export default defineStack({
  datasources: [BusinessDb, AnalyticsReplica],
  datasourceMapping: [
    { namespace: 'biz',            datasource: 'business_primary' },
    { objectPattern: 'report_*',   datasource: 'analytics_replica' },
    { package: 'com.example.logs', datasource: 'default' },
    { default: true,               datasource: 'default' },
  ],
});

A rule matches by namespace, package, objectPattern (glob), or default, and names the target datasource. This keeps data-residency decisions in one place instead of scattered across object files.

Generating objects from existing tables

You don't have to hand-write an object for every table. The fastest way to bring a legacy schema into ObjectOS today is to use a coding agent (Claude Code) to scan the business tables and generate source-level object definitions — one *.object.ts file per table, in exactly the shape the framework expects.

The hotcrm reference app is the canonical example of that shape: each table is a src/objects/<name>.object.ts file using ObjectSchema.create({ … }) with Field.* definitions, all assembled by defineStack. A typical flow:

  1. Connect the business database as a datasource (above).
  2. Point Claude Code at the schema. Ask it to introspect the connected database — table names, columns, types, foreign keys — and generate one ObjectSchema.create file per table, mapping SQL columns to Field.* types and foreign keys to Field.lookup(...). Set each object's datasource to your connection (or rely on datasourceMapping), and external.remoteName where the table name differs from the object name.
  3. Review & refine the generated objects — add labels, field groups, validations, and permissions. The output is ordinary source you own and commit, just like hotcrm/src/objects/*.object.ts.
  4. Run. The objects now read your existing tables through the bound datasource. At boot the runtime checks each one against its table (external.validation), and by default a mismatch fails the boot. Writes stay off until both gates open: external.allowWrites: true on the datasource and external.writable: true on the object.

Because the generated objects are plain source, you get full control: keep what fits, drop columns you don't want to expose, and layer ObjectOS features (history tracking, activities, sharing rules) on top of a database the platform never had to own.

Query pushdown

What runs in the database and what the engine evaluates is decided by each driver itself, not by datasource metadata. A filter on a federated object, such as ?tier=premium, is pushed down to the remote query.

There is no capabilities block to tune. It was removed in ObjectStack 17.0 because nothing read it: declaring a capability never changed which path ran, and its readOnly flag never stopped a write. For read-only access, see Security notes.

AI over connected data

Once tables are modeled as objects, the AI layer works on them for free. ObjectOS's agents and tools — list_objects, describe_object, query_records, aggregate_data, and the data-chat agent — all go through ObjectQL, which routes each object to its bound datasource. That means:

  • A user can ask questions in natural language about data that lives in the legacy business system, and the answer is computed against the real rows.
  • Tool calls and queries respect the calling user's permissions — AI never sees more than the signed-in user is allowed to.
  • The same agents, flows, and dashboards work whether an object is backed by the primary database or an external business system.

See AI Service and AI Agents for how to wire up the chat and agent layers, and Runtime for the primary-database configuration that backs the default datasource.

Security notes

  • Never inline credentials. Put the password in the secrets store and reference it with external.credentialsRef, as shown above.
  • Use a read-only database user for systems you don't intend to write to — grant it SELECT only. Then nothing that reaches the database through that connection can write, inside ObjectOS or not. On an external datasource, external.allowWrites: false — the default — also refuses writes in ObjectOS's own query path. There is no readOnly key: the one that existed never gated a write, and ObjectStack 17.0 removed it.
  • Scope with permissions. Object- and field-level permissions apply to connected data exactly as they do to native objects.

In-product external datasource federation

The flow above — connect a database, model objects, generate them with a coding agent — runs on turn-key federation: schemaMode: 'external', specified in ADR-0015 (status: Accepted), ships today: bind objects to tables ObjectOS does not own, with validation gates around them. It is read-only by default and early: expect the read path to be the mature one.

It is documented once, with the framework docs that own the surface — External Datasources (Federation).

Where to go next