Getting started

Create a typed OpenSya Persistence runtime with PostgreSQL and Drizzle.

Install

Terminal
pnpm add @opensya/persistence drizzle-orm pg
pnpm add -D @types/pg

Persistence currently ships a Drizzle PostgreSQL adapter. Your physical tables must already exist through migrations.

Define a table

Use defineTable() instead of annotating the object with TableMetadata. The helper validates the shape while preserving literal names for type inference.

users.metadata.ts
import { defineTable } from '@opensya/persistence'

export const usersMetadata = defineTable({
  name: 'users',
  collectionName: 'users',
  columns: [
    {
      name: 'id',
      columnName: 'id',
      type: 'uuid',
      nullable: false,
      primaryKey: true,
      unique: true,
      default: () => crypto.randomUUID(),
      validators: []
    },
    {
      name: 'email',
      columnName: 'email',
      type: 'string',
      nullable: false,
      primaryKey: false,
      unique: true,
      validators: [
        {
          name: 'email-format',
          validate(value) {
            return typeof value === 'string' && value.includes('@')
              ? { valid: true }
              : { valid: false, message: 'Enter a valid email address.' }
          }
        }
      ]
    },
    {
      name: 'createdAt',
      columnName: 'created_at',
      type: 'timestamp',
      nullable: false,
      primaryKey: false,
      unique: false,
      default: () => new Date(),
      validators: []
    }
  ],
  relations: [],
  tableValidators: []
})

Create the runtime

persistence.ts
import {
  createDrizzleAdapter,
  createHooksRegistry,
  createMetadataRegistry,
  createQueryEngine
} from '@opensya/persistence'
import { usersMetadata } from './users.metadata.js'

const registry = createMetadataRegistry(usersMetadata)
registry.lock()

const adapter = createDrizzleAdapter(db)

for (const table of registry.getAll()) {
  adapter.buildTable(table)
}

const hooks = createHooksRegistry()
export const engine = createQueryEngine(registry, adapter, hooks)

buildTable() builds the runtime Drizzle objects. It does not execute DDL.

You can also keep registration chainable:

const registry = createMetadataRegistry()
  .register(usersMetadata)
  .register(postsMetadata)

registry.lock()

Execute typed operations

users.service.ts
const user = await engine.create('users', {
  email: 'john@example.com'
})

// user is inferred as:
// { id: string; email: string; createdAt: Date }

const saved = await engine.findOne('users', {
  where: {
    conditions: [
      { field: 'id', operator: 'eq', value: user.id }
    ]
  }
})

No User interface or explicit generic is required. If an application needs a custom projection, explicit generics remain supported.

const user = await engine.findOne<PublicUser>('users', { where })

Next

Metadata and registry
Learn inference, indexes, audit settings and registry rules.
Queries and filters
Compose filters, sorting and safe mutations.
Production playground
Run the complete package against PostgreSQL.