zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Operations and deployment

EncoreSupported

Encore is infrastructure-from-code: you declare services and resources in TypeScript, and Encore provisions the database, the topics and the deployment. That overlaps with zmdb in one place and complements it everywhere else.

What overlaps and what does not#

Encorezmdb
Provisioning the databaseyesno
Service definitions, API endpointsyes@zmdb/web
Request validationfrom the endpoint's typesAOT validators
Migrationsits own runner, migrations/*.up.sqlits own runner
Schema declarationplain SQLa tagged interface
Typed queriestagged templatesquery compiler + repository
Tracing, dashboardsbuilt innone

So the sensible arrangement is: Encore owns infrastructure, endpoints and migrations; zmdb owns the schema, the queries and the row types. Do not run two migration systems against one database, and do not wrap Encore's endpoints in @zmdb/web — you would lose the tracing and typed clients that are the reason to use Encore.

A driver over Encore's database#

import { SQLDatabase } from 'encore.dev/storage/sqldb';
import { type Driver } from '@zmdb/orm';

const db = new SQLDatabase('app', { migrations: './migrations' });

export const driver: Driver = {
  async execute(query) {
    const rows: Record<string, unknown>[] = [];
    for await (const row of db.rawQuery(query.text, ...query.parameters)) rows.push(row);
    return rows;
  },
};

rawQuery is variadic rather than array-taking, hence the spread. CompiledQuery.parameters is a readonly unknown[], which spreads fine.

Everything downstream now works: defineRepository, Entity<S>, CreateDTO<S>, populate, aggregate.

Migrations: use Encore's#

Encore provisions the database and expects to own its schema, and it applies migrations/1_x.up.sql on deploy. Generate the SQL from your schemas and commit it:

// scripts/emit-migration.ts
import { diff, emitUp, snapshot } from '@zmdb/core/migrations';
import { writeFileSync } from 'node:fs';
import { allSchemas } from '../src/schema.js';

const ops = diff(previousSnapshot, snapshot(allSchemas));
writeFileSync('migrations/2_add_posts.up.sql', ops.map(o => emitUp(o, 'postgres')).join(';\n') + ';\n');

You keep the declaration as the source of truth and Encore keeps its own runner. Review the emitted SQL before committing — the generated form is correct but not always what you would write by hand, and Encore's migrations are irreversible in production.

Do not also call up(...). Two runners with two version tables against one database is a schema you cannot reason about.

Validation#

Encore derives validation from an endpoint's request type, which covers the boundary. So the AOT validators are largely redundant here — and Encore compiles with its own toolchain, so the transformer does not run:

it('the transformer is running', () => {
  expect(is<{ id: number }>({ id: 'x' })).toBe(false); // expect this to fail under Encore
});

Rely on Encore's endpoint validation, and use zmdb's assert only in modules you compile yourself. Do not assume assert is checking anything inside an Encore service. See AOT Setup.

Transactions#

await using tx = await db.begin();

Encore's transaction handle is its own. Since zmdb's withTransaction needs a driver bound to the transaction, build one per transaction:

function txDriver(tx: Transaction): Driver {
  return {
    async execute(query) {
      const rows: Record<string, unknown>[] = [];
      for await (const row of tx.rawQuery(query.text, ...query.parameters)) rows.push(row);
      return rows;
    },
  };
}

const repo = defineRepository(posts, txDriver(tx));

The repository is an object over a driver, so constructing one per transaction costs nothing. This is the general pattern for any framework that owns its own transaction handle. See Transactions.

Where the fit is genuinely poor#

What is left is the part worth having: one typed schema definition, derived DTOs, a query compiler that produces plain SQL, and a repository that does not need an engine. Encore provides the rest.

---

See also: Writing a Driver · Transactions · Deployment