Skip to content

Both, with Postgres

The live example is a server and a React app over thirteen collections: companies, tickets, appointments and an org chart in Postgres, files from a directory, flights, earthquakes and GitHub repositories from the net, the menu from a YAML file. Run it, then do the same with a collection of your own.

You need Node 24 and Postgres on localhost with a role jsudc (password 123, CREATEDB); backend/.env.example has the variables that point elsewhere.

  1. Clone and install:

    Terminal window
    git clone https://github.com/digital-cube/jsudc-example.git && cd jsudc-example
    nvm use && npm ci
  2. The databases. db:setup creates jsudc and jsudc_test and applies the migrations to both: the example’s eight tables and the UDC tables (the schema revisions of every Postgres collection).

    Terminal window
    npm run db:setup
  3. The rows. seed writes every Postgres collection’s schema.json as schema revision 1 and inserts its rows; --reset empties them first, back to the files.

    Terminal window
    npm run seed -- --reset
  4. Start both halves:

    Terminal window
    npm run dev # server on :5100 (OpenAPI UI at /docs), React app on :5173

    Open localhost:5173/udc/companies; every collection is in the sidebar.

A collection directory without a source.yaml is a Postgres table named like the directory, each row a jsonb document validated against the collection’s active schema revision. Take the books directory of the server quick start, drop its source.yaml, and keep schema.json, view.yaml and seed.json.

  1. Install the server with its Postgres peers:

    Terminal window
    npm install @jsudc/server @jsudc/core fastify zod drizzle-orm pg
  2. Create the tables and seed them, once:

    seed.ts
    import { readFileSync } from "node:fs";
    import {
    connect,
    createCollectionTableSql,
    ensureUdcSchema,
    readCollectionAssets,
    seedCollection,
    } from "@jsudc/server";
    import { sql } from "drizzle-orm";
    const handle = connect(process.env.DATABASE_URL ?? "postgresql://localhost/app");
    await ensureUdcSchema(handle.db);
    for (const asset of readCollectionAssets("./collections")) {
    if (asset.source.kind !== "postgres_jsonb" || !asset.seedFile) continue;
    await handle.db.execute(sql.raw(createCollectionTableSql(asset.name)));
    const res = await seedCollection(handle.db, {
    table: asset.name,
    schema: asset.schemaJson,
    rows: JSON.parse(readFileSync(asset.seedFile, "utf8")),
    });
    console.log(`${res.table}: ${res.inserted} rows, schema revision 1 ${res.revision}`);
    }
    await handle.close();
    Terminal window
    DATABASE_URL=postgresql://localhost/books node seed.ts
    # books: 3 rows, schema revision 1 created
  3. Serve them. The only differences from the memory server are the connection and ensureUdcSchema, which creates the UDC tables when they are missing:

    server.ts
    import {
    connect,
    createDispatcher,
    ensureUdcSchema,
    loadRegistry,
    makeSource,
    udcPlugin,
    } from "@jsudc/server";
    import Fastify from "fastify";
    const { db } = connect(process.env.DATABASE_URL ?? "postgresql://localhost/app");
    await ensureUdcSchema(db); // the UDC tables: schema revisions; or apply migrations/*.sql yourself
    const registry = loadRegistry("./collections", (asset, registry) =>
    makeSource(asset, { db, registry, env: process.env }),
    );
    const app = Fastify({ logger: true });
    const dispatcher = createDispatcher(registry, { log: app.log });
    await app.register(udcPlugin, { dispatcher, registry, swagger: false });
    await app.listen({ port: 5100 });
    Terminal window
    DATABASE_URL=postgresql://localhost/books node server.ts

The React data view draws it unchanged. Now the schema editor writes: a column added in the browser is a new schema revision in udc_schema_revisions, and a field marked index: true gets its expression index.