Aviato / docs
Plugins & integrations

Typed plugins and local development

Generate collection and field names for plugin code from your database, and reload a plugin without restarting the connector.

View Markdown

Plugin code names collections and fields as strings: plugin.action("orders", …). Nothing checks them while you write the code, so a typo shows up only at runtime, as an action missing from the dashboard or a hook that never runs. aviato-connector typegen reads the same schema the connector introspects and writes it out for your language, so the compiler catches the typo instead.

Generate types

Run typegen with the database settings the connector uses. It needs only AVIATO_DATABASE_URL (and any AVIATO_DATASOURCE_<NAME>_URL), not a project token, so it also runs in CI against a migrated test database.

aviato-connector typegen --out src/aviato.ts               # TypeScript
aviato-connector typegen --out internal/aviato/schema.go   # Go
aviato-connector typegen --out app/Aviato                  # PHP, one class per collection

The output path selects the language: a .ts or .go file, or a directory for PHP. Pass --lang to override it, --package to name the Go package (the directory name by default), and --namespace for PHP (App\Aviato by default). Run it again after each migration, and commit the result. --check writes nothing and exits with status 1 when the files are out of date. Use it in CI to catch a migration whose types were not regenerated.

The generated AviatoSchema type lists each collection with its fields, typed as the SDK receives them in JSON: dates, UUIDs and binary values are strings, and nullable columns accept null.

aviato.ts
// Generated by `aviato-connector typegen` from your database schema. Do not edit: run it again after a migration.
// Pass it to the SDK so collection and field names are checked: createAviatoPlugin<AviatoSchema>({ ... }).

export type AviatoSchema = {
    /** The `customers` collection. */
    customers: {
        fields: {
            id: number;
            email: string;
            plan: "free" | "pro";
            stripe_customer_id: string | null;
        };
    };
    /** The `orders` collection. */
    orders: {
        fields: {
            id: number;
            customer_id: number;
            total: number;
            refunded_at: string | null;
        };
    };
};

export type AviatoCollection = keyof AviatoSchema;

/** A record of `C` as the SDK receives it. */
export type AviatoRecordOf<C extends AviatoCollection> = AviatoSchema[C]["fields"];

Pass it to createAviatoPlugin. Collection names in actions, computed fields, hooks, segments, search, charts, summaries and write overrides must then exist, and computed-field dependencies and write-override fields must belong to their collection:

plugin.ts
import { createAviatoPlugin } from "@getaviato/sdk";
import type { AviatoRecordOf, AviatoSchema } from "./aviato";

export function createTypedPlugin(secret: string) {
    return createAviatoPlugin<AviatoSchema>({ secret, basePath: "/aviato" })
        .action({
            collection: "orders",
            name: "Refund",
            scope: "single",
            execute: async (context) => {
                const [order] = (await context.getRecords()) as AviatoRecordOf<"orders">[];
                if (!order || order.refunded_at !== null) {
                    return context.error("Select one order that is not refunded yet");
                }
                return context.success(`Refunded ${order.total}`);
            },
        })
        .computedField({
            collection: "customers",
            name: "email_domain",
            type: "string",
            // A field the customers table does not have fails to compile.
            dependencies: ["email"],
            compute: (records) => records.map((record) => String(record.email).split("@")[1] ?? null),
        })
        .layout("customers", {
            label: "Clients",
            icon: "users",
            // Field names are checked against the schema too.
            fieldOrder: ["email", "plan"],
            fields: { plan: { widget: "badge" }, stripe_customer_id: { label: "Stripe customer", readOnly: true } },
            searchFields: ["email"],
            defaultSort: "-id",
        });
}

Without the type parameter, the SDK accepts any string, as before. Collections served by a custom datasource are declared by your plugin, not the database. Add them to the type yourself: createAviatoPlugin<AviatoSchema & { tickets: { fields: { id: string; subject: string } } }>(…).

Develop a plugin locally

aviato-connector dev runs the connector for local development. It differs from start in three ways:

  • The manifest reloads every second. Restart your plugin (or let bun --watch, air or php artisan serve restart it) and the new actions, hooks and fields appear in the dashboard without restarting the connector. While the plugin restarts, the connector keeps the last manifest it loaded.
  • It waits for the plugin. start refuses to run when the plugin does not answer, so it never serves data without your hooks and write overrides. dev waits until the plugin answers instead, and serves nothing until then.
  • The database is re-introspected every ten seconds, so a migration shows up as you run it. With --types, the generated files are rewritten when the schema changes.
aviato-connector dev --types src/aviato.ts

dev loads aviato.env from the current directory when it exists (--env-file picks another); variables already set in your shell take precedence. Separate several --types outputs with commas. With Laravel, run php artisan aviato:serve --dev --types=app/Aviato.

Use aviato-connector start in production. To reload a running production connector after a plugin deployment, send it SIGHUP (kill -HUP <pid>). It fetches the manifest and re-introspects the database at once, instead of waiting for the next minute's refresh.

Check Aviato changes on pull requests

A migration that drops a column, or a refactor that renames an action, changes what your team sees in the back-office. aviato-connector snapshot records what Aviato sees at one commit: the introspected database schema and, when AVIATO_PLUGIN_URL and AVIATO_PLUGIN_SECRET are set, the manifest of the running plugin. aviato-connector diff compares two snapshots and lists what was added, changed and removed.

aviato-connector snapshot --output base.json              # on the target branch
aviato-connector snapshot --output head.json              # on the pull request
aviato-connector diff --base base.json --head head.json   # without --head: compares with a fresh snapshot

Like typegen, both commands need only the database variables, not a project token. --schema reads the schema from a JSON file instead of a database, and --manifest reads the manifest from a file (the protocol JSON that GetManifest returns) instead of calling the plugin. Without a plugin, the snapshot's manifest is null.

diff --format prints text (the default), markdown for a pull request comment, or json. With --fail-on-breaking, it exits with status 1 when a change is breaking. A change is breaking when it can break a bookmark, a saved view, an API call or plugin code:

  • a removed collection, field, relation, action, segment, chart or computed field;
  • a changed field type (computed fields included), or a primary key change;
  • a field that becomes non-nullable without a default, so creating a record without it fails;
  • an action whose scope changes (single, bulk or global);
  • a dangling reference: a layout, summary, widget, computed-field dependency, relation or other plugin definition of the pull request that names a collection or field its schema no longer has. Collections served by a custom datasource are not checked.

Additions, hooks, search, write overrides, and changes to layouts, summaries and widgets are reported without failing the check.

The Markdown output starts with an <!-- aviato-diff --> marker and lists breaking changes first. The workflow below migrates a test database to the target branch, takes a snapshot with the plugin of that branch running, then does the same for the pull request and posts the comparison as a single comment that each push updates:

.github/workflows/aviato-diff.yml
name: Aviato changes
on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  diff:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
        ports: ["5432:5432"]
        options: --health-cmd pg_isready --health-interval 5s --health-retries 10
    env:
      CONNECTOR_IMAGE: getaviato/connector:0.3.0
      AVIATO_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
      AVIATO_PLUGIN_URL: http://localhost:3000/aviato
      AVIATO_PLUGIN_SECRET: ${{ secrets.AVIATO_CI_PLUGIN_SECRET }} # any whsec_ secret, shared with the test plugin
    steps:
      - name: Snapshot the target branch
        uses: actions/checkout@v4
        with:
          ref: ${{ github.base_ref }}
      - run: |
          npm ci && npm run db:migrate
          npm run start:plugin & plugin=$!
          npx wait-on tcp:localhost:3000
          docker run --rm --network host -e AVIATO_DATABASE_URL -e AVIATO_PLUGIN_URL -e AVIATO_PLUGIN_SECRET \
            "$CONNECTOR_IMAGE" snapshot > "$RUNNER_TEMP/base.json"
          kill $plugin

      - name: Snapshot the pull request
        uses: actions/checkout@v4
      - run: |
          npm ci && npm run db:migrate
          npm run start:plugin &
          npx wait-on tcp:localhost:3000
          docker run --rm --network host -e AVIATO_DATABASE_URL -e AVIATO_PLUGIN_URL -e AVIATO_PLUGIN_SECRET \
            "$CONNECTOR_IMAGE" snapshot > "$RUNNER_TEMP/head.json"

      - name: Compare
        run: |
          docker run --rm -v "$RUNNER_TEMP:/snapshots:ro" "$CONNECTOR_IMAGE" \
            diff --base /snapshots/base.json --head /snapshots/head.json --format markdown > aviato-diff.md
      - uses: marocchino/sticky-pull-request-comment@v2
        with:
          header: aviato-diff
          path: aviato-diff.md
      - name: Fail on breaking changes
        run: |
          docker run --rm -v "$RUNNER_TEMP:/snapshots:ro" "$CONNECTOR_IMAGE" \
            diff --base /snapshots/base.json --head /snapshots/head.json --fail-on-breaking

Replace db:migrate and start:plugin with your own commands. The pull request's migrations run on top of the target branch's, as they will in production. Without a plugin, leave out the plugin variables and steps. To keep a known-good snapshot instead of rebuilding the target branch each time, commit aviato-snapshot.json and run aviato-connector diff --base aviato-snapshot.json against the migrated database.

Edit on GitHub

On this page