Concepts

Architecture

How metadata, execution services, and database adapters work together.

OpenSya Persistence separates the description of a data model from the execution of database operations.

Runtime overview

Application service
        │
        ▼
    QueryEngine
   ┌────┼──────────────┐
   ▼    ▼              ▼
Registry Hooks   RelationResolver
        │              │
        └──────┬───────┘
               ▼
       DatabaseAdapter
               │
               ▼
            Database

Components

Read path

Resolve table metadata

Unknown logical table names fail through registry.getOrThrow().

Query the adapter

The engine forwards filters, sorting, limit, and offset.

Populate requested relations

When populate is present, the resolver loads each named direct relation.

Serialize and return typed data

Hidden and contextual fields are removed, including inside populated relations. The entity type is inferred from the registered table metadata.

Reads do not run lifecycle hooks and are not automatically transactional.

Mutation path

Defaults → before hooks → known-field check → complete validation → insert → after hooks.

All mutation steps execute inside adapter.transaction(). When configured, audit entries use that same adapter. Application transactions additionally flush collected Domain Events to the outbox before the outer commit.

Logical and physical identifiers

PropertyUsed byExample
TableMetadata.nameEngine and adapter lookupusers
TableMetadata.collectionNamePhysical table construction and consistency matchingapp_users
ColumnMetadata.nameEntities, filters, and sortingcreatedAt
ColumnMetadata.columnNamePostgreSQL column construction and consistency matchingcreated_at

Adapter independence

The Query Engine does not import Drizzle types. A custom adapter can target another store if it preserves the contract and semantics.

A custom adapter should preserve non-empty mutation filters, field validation, transactional callbacks, predictable return values, and read-only introspection.