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:
| Field | Purpose |
|---|---|
name | Unique identifier (^[a-z_][a-z0-9_]*$) objects reference |
label | Human-readable display name |
driver | Which driver handles the connection (postgres, mysql, sqlite, mongodb, memory, or a plugin-contributed driver) |
config | Driver-specific connection settings (host, database, credentials, …) |
pool | Connection-pool sizing (min, max, timeouts) |
schemaMode | Who 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 |
external | The federation policy of an external datasource — its write gate and boot validation — plus credentialsRef, the secrets-store reference any datasource may carry |
active | Whether 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 package | driver | Backends |
|---|---|---|
@objectstack/driver-sql | postgres, mysql, sqlite | PostgreSQL, MySQL, SQLite (via knex) |
@objectstack/driver-mongodb | mongodb | MongoDB |
@objectstack/driver-memory | memory | In-process, for tests — not a boot store since ObjectStack 17.7 |
@objectstack/driver-sqlite-wasm | sqlite (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 theDatasourceSchemavalidation at the point of declaration, so a bad field is reported in the file that has it. The framework'sexamples/app-crmstack declares its datasources this way. A plain object typedDatasource(from@objectstack/spec/data) is also accepted, and is validated when the stack is assembled.Credentials are never inline.
DatasourceSchemarefuses apasswordinconfig, even one read from the environment: the datasource is stored whole insys_metadata, so an inline secret would sit there in cleartext. Put the secret in the secrets store and name it inexternal.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
externaldatasource withvalidation.onMismatch: 'fail'fail-fasted: an app declaringdatasource: '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 withDatasource 'x' is not registered./api/v1/readyalso 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:
- Connect the business database as a datasource (above).
- Point Claude Code at the schema. Ask it to introspect the
connected database — table names, columns, types, foreign keys — and
generate one
ObjectSchema.createfile per table, mapping SQL columns toField.*types and foreign keys toField.lookup(...). Set each object'sdatasourceto your connection (or rely ondatasourceMapping), andexternal.remoteNamewhere the table name differs from the object name. - 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. - 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: trueon the datasource andexternal.writable: trueon 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
SELECTonly. 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 noreadOnlykey: 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
- Runtime — the primary database behind
default - AI Service — chat, embeddings, RAG, MCP
- AI Agents — declarative agents over your objects
- Data Model — the
ObjectSchema.createauthoring surface examples/app-crmdatasources — a real datasource + routing example