DevTools & tooling

Actions

Named operations you run from the DevTools tab, next to the database they need.

An action is an operation you run on demand while developing: seeding, a reset, a migration CLI, a studio. It is defined next to what it needs and runs there; only its id, label and description, and then its result, reach the tab.

There are three kinds, by where they run:

KindDefined inRunsReceives
serverdevtoolsActions in server/pglite.config.tsnext to the server instance{ pg }
clientdevtoolsActions in app/pglite.config.tsin the app's tab, next to its worker instance{ pg }
socketpglite.socket.devtoolsActions in nuxt.config.tsin the Nuxt process, next to the socket{ socketUrl, env, dataDir, startSubprocess, terminal, logger }

Every action has an id (unique per kind), a label, an optional description and a run function. What run returns is shown in the tab, reduced to JSON (dates as ISO strings, bigint as a string, binary data as its size); what it throws is shown as the error.

Server actions

Under devtoolsActions in the server config, run against the server instance:

server/pglite.config.ts
export default definePGliteServerConfig({
  devtoolsActions: [
    {
      id: 'seed',
      label: 'Seed the database',
      description: 'Inserts a few rows into todos',
      run: async ({ pg }) => {
        const { affectedRows } = await pg.query(
          `INSERT INTO todos (title) VALUES ('Read the docs'), ('Try the socket')`,
        )
        return affectedRows
      },
    },
  ],
})

With the development socket running, they run against the socket's instance and queue like any other client's statements. Without it, they run in Nitro through usePGlite(). They are never part of a build.

Client actions

Under devtoolsActions in the client config, run in the app's tab against its worker instance:

app/pglite.config.ts
export default definePGliteClientConfig({
  devtoolsActions: [
    {
      id: 'clear',
      label: 'Clear local todos',
      run: async ({ pg }) => (await pg.query('DELETE FROM todos')).affectedRows,
    },
  ],
})

Socket actions

In nuxt.config.ts, under pglite.socket.devtoolsActions, run in the Nuxt process next to the development socket: the place for CLIs and tools that reach the database through the socket URL, as any Postgres client would. They exist only while the socket runs. startSubprocess starts a long-running command whose output streams to its own terminal in DevTools:

nuxt.config.ts
import type { PGliteSocketAction } from 'nuxt-pglite'

const migrate: PGliteSocketAction = {
  id: 'migrate',
  label: 'Run migrations',
  description: 'node-pg-migrate up, against the development socket',
  run: ({ env, startSubprocess }) => {
    startSubprocess(
      { command: 'npx', args: ['node-pg-migrate', 'up'], env },
      { id: 'migrate', name: 'Migrations' },
    )
  },
}

export default defineNuxtConfig({
  pglite: {
    socket: { port: 5433, devtoolsActions: [migrate] },
  },
})

env holds the variables the socket exported (DATABASE_URL by default), so any CLI that reads them reaches the socket as the app does.

The context has:

  • socketUrl: the development socket's URL.
  • env: the variables the socket exported, with their values, to pass on to a command.
  • dataDir: the data directory the socket serves; unset for an in-memory database.
  • startSubprocess(execaOptions, tabOptions): startSubprocess from @nuxt/devtools-kit, already bound to this Nuxt instance.
  • terminal: the terminal, cooperating with the nuxt dev UI when it runs.
  • logger: the module's logger.

This site's own action opens Drizzle Studio, whose config points at the socket's pinned port:

nuxt.config.ts
import type { PGliteSocketAction } from 'nuxt-pglite'

const studio: PGliteSocketAction = {
  id: 'drizzle-studio',
  label: 'Open Drizzle Studio',
  run: ({ startSubprocess }) => {
    startSubprocess(
      { command: 'pnpm', args: ['exec', 'drizzle-kit', 'studio'] },
      { id: 'drizzle-studio', name: 'Drizzle Studio', icon: 'simple-icons:drizzle' },
    )
    return 'Drizzle Studio is starting, see the DevTools terminal.'
  },
}

Reset database

The module puts a socket action of its own first, Reset database (reset-database). It closes the socket's instance, deletes its data directory, creates it again (init included, so migrations and seeds run from scratch) and serves it behind the same URL, so the exported variables stay valid. Connected clients are disconnected, and reconnect as usual.

  • An in-memory database is simply recreated.
  • A directory that does not look like PGlite's is refused, rather than deleted.
  • A failed reset leaves the socket refusing clients with the new reason.
  • It acts on the socket's instance only: one created by usePGlite() in Nitro is not affected.

It is the way out of edited migrations, or of an init that fails on data left by an earlier run.

Typing actions

Inline actions are typed from their config, so pg carries your extensions. An action written on its own takes the matching type:

import type { PGliteClientAction, PGliteServerAction, PGliteSocketAction } from 'nuxt-pglite'

PGliteServerAction takes the config's extensions as a type parameter, PGliteClientAction its clientExtensions (PGliteClientAction<{ live: typeof live }>), when run needs them.

Actions from other modules

Modules add socket actions through a hook, called in development once every module is set up, while the socket runs:

my-module/src/module.ts
export default defineNuxtModule({
  setup(_options, nuxt) {
    nuxt.hook('pglite:devtools:actions', (actions) => {
      actions.push({
        id: 'my-module:studio',
        label: 'Open my studio',
        run: ({ socketUrl }) => {
          // …
        },
      })
    })
  },
})

Prefix the id with your module's name: ids are unique per kind, and a duplicate fails the startup.