Ignition 8.3+ Gateway Module

Schema migrations,
version-controlled
like everything else.

SchemaForge compiles a code-first schemaforge DSL into a structural diff against your live datasource, classifies every change as safe or destructive, and applies it through Liquibase — DBMS-agnostic by construction, and never automatic.

Ignition 8.3+ MSSQL & Postgres Liquibase-backed DBMS-agnostic
schemas/orders.schemaforge
# edit a schema resource, save table "orders" { column "status" varchar(20), default("pending") index "idx_orders_status" on ("status") __environment__ ["prod", "staging"] } # resolved against last snapshot applied to prod >>> diff vs. prod: 2 pending changes safe +1 column (status) safe +1 index (idx_orders_status) destructive 0 >>> awaiting operator review — Pending Changes page

Structural diff engine

Every save or Gateway restart diffs the resource's Model IR against the last snapshot actually applied to each datasource.

Safe vs. destructive, always

Drops and column type changes are classified destructive and never applied without an explicit confirm-gated action.

DBMS-agnostic by construction

Liquibase handles the dialect differences — live-verified end-to-end against real MSSQL and Postgres datasources.

Nothing applies automatically

A save or restart only ever logs what's pending. Applying schema changes is a deliberate, audited operator action.

Why It Exists

Schema changes deserve the same rigor as code changes.

Most Ignition projects track schema changes as ad hoc SQL scripts, if they're tracked at all — with no diff, no classification, and no record of what's actually pending against which environment.

Without SchemaForge

  • Schema changes live in ad hoc SQL scripts, or in nobody's head
  • No visibility into what's pending before it's applied
  • Destructive changes run with the same confidence as additive ones
  • Reverse-engineering an existing database means reading DDL by hand
  • Seed scripts risk duplicate rows on every rerun

With SchemaForge

  • Schemas and seeds are versioned Designer project resources, like anything else
  • A Pending Changes page lists every outstanding change, safe and destructive, before anything runs
  • Destructive changes require an explicit confirm-gated action
  • A Reverse Engineer wizard generates a schema resource from live DatabaseMetaData
  • Seeds upsert idempotently by key — safe to rerun against a populated datasource

How It Works

Author, diff, classify, apply.

01 / Author ✍️

Write the DSL

A schema or seed resource is edited as plain schemaforge DSL text in the Designer, compiled to a Model IR. A resource that doesn't compile doesn't save.

02 / Diff 🔍

Resolve and diff

On save or Gateway restart, __environment__ tags resolve to real datasources and the Model IR is diffed against the last applied snapshot.

03 / Classify ⚖️

Compile the changeset

Every change compiles to a Liquibase changeset and is classified safe (additive) or destructive (drops, type changes) — no in-between.

04 / Apply ✅

Operator reviews and applies

The Pending Changes page lists outstanding changes per resource. Apply safe changes, or explicitly confirm the destructive ones.

Module Architecture

One Gradle project, four scopes.

Built with the io.ia.sdk.modl Gradle plugin as an installable Ignition 8.3 module.

common

DSL parser/compiler, Model IR, diff engine, JSON (de)serialization, reverse-engineering DSL writer, Designer↔Gateway RPC interface.

designer

Resource workspaces/editors for Schemas and Seeds, plus the reverse-engineering wizard.

gateway

Resource lifecycle observation, Liquibase changeset compilation and execution, seed upsert execution, database introspection, environment→datasource settings, REST routes.

web-ui

React/TypeScript Gateway pages — settings, Pending Changes, Seeds — built with Webpack into a SystemJS module bundled into the Gateway jar.

Two Resource Types

Schemas and seeds, as compiled DSL text.

Both compile to a plain-Java Model IR in the Designer, so the Gateway never has to parse DSL itself — it works from the compiled IR stored alongside the resource.

Schema resource

// tables, columns, types, keys, indexes table "orders" { column "id" int, primary_key, autoincrement column "customer_id" int, not_null column "status" varchar(20), default("pending") foreign_key "customer_id" -> "customers.id" index "idx_orders_status" on ("status") __environment__ ["prod", "staging"] }

Seed resource

// upserted idempotently by __key__ __model__: "orders" __environment__: ["prod", "staging"] __key__: "id" rows: - id: 1, customer_id: 42, status: "pending" - id: 2, customer_id: 43, status: "shipped" // autoincrement id + explicit value: handled via // SET IDENTITY_INSERT (MSSQL) transparently

Reverse Engineering

Start from a database that already exists.

"Reverse Engineer from Database..." on the Schemas folder introspects an existing datasource and generates a real schema-model resource — through the same creation flow, and the same compile-or-it-doesn't-save gate, as anything hand-written.

01 / Pick 🗂️

Choose a datasource

Launch the wizard from the Schemas folder in the Designer and point it at an existing Ignition datasource connection.

02 / Introspect 🔬

Read DatabaseMetaData

Columns, types, primary/foreign keys, and indexes are read directly via JDBC DatabaseMetaData.

03 / Generate 📝

Write the DSL

The introspected structure is written out as schemaforge DSL source, not just an opaque IR blob.

04 / Gate 🚪

Same save gate as hand-written

The generated resource must compile to save — no special-cased path for reverse-engineered schemas.

Gateway Web UI

Three pages, one React bundle.

A React/TypeScript, SystemJS-mounted Gateway page, reusing Ignition's own pooled JDBC connections.

Environment → Datasource

Maps each __environment__ tag used in resources to a real Ignition datasource connection, resolved on every save and restart.

Pending Changes

Every resource's outstanding safe and destructive changes, listed separately, with "apply safe changes" and a confirm-gated "apply including destructive" action.

Seeds

Every enabled seed and its resolved datasource(s), run on demand — idempotent upserts, safe against a datasource that already has rows.

Not Built Yet

Functional end-to-end. Not finished.

Live-verified against real MSSQL and Postgres datasources — with known gaps, stated plainly rather than papered over.

Building

Requires JDK 17.

01

Build the module

Produces build/SchemaForge.unsigned.modl always, plus a signed build/SchemaForge.modl if signing credentials are configured.

./gradlew build
02

Sign locally (optional)

A self-signed keypair works for dev/internal use — the Gateway prompts to trust the certificate once, rather than requiring -Dignition.allowunsignedmodules=true on every reinstall. Signing properties live in ~/.gradle/gradle.properties, never in the repo.

03

Deploy to a dev Gateway

deployModl pushes to localhost:8088 by default — override tasks.deployModl.hostGateway or pass -PhostGateway=... for a different target.

./gradlew deployModl
04

Restart required

Ignition 8.3 does not hot-reload modules — every change means reinstalling the .modl and restarting the Gateway, and reconnecting the Designer for designer-scope changes.

Get in touch

Bring versioned, classified migrations to the gateway you already run.

Happy to walk through the DSL, the diff/classification engine, or the reverse-engineering wizard for a specific schema.

eric.e.fredericksen@gmail.com ← Back to portfolio