Skip to content

sandros94/nuxt-pglite

Repository files navigation

Nuxt PGlite

npm version npm downloads License Nuxt

A Nuxt module aimed to simplify the use of PGlite.

PGlite, an Embeddable Postgres Run a full Postgres database locally in WASM with reactivity and live sync.

Warning

No docs are available (although planned), please refer to the playground code.

Features

  • ⚡️ Server-side usePGlite, running in your Node or Bun server.
  • 🧑‍💻 Client-side usePGlite, running inside Web Workers.
  • 🪢 Client-side useLiveQuery and useLiveIncrementalQuery to subscribe to live changes.

Quick Setup

Install the module to your Nuxt application with one command:

npx nuxi module add nuxt-pglite

That's it! You can now use Nuxt PGlite in your Nuxt app ✨

Storage

You can configure where to store data in your nuxt.config.ts. Server-side storage accepts relative baths:

export default defineNuxtConfig({
  modules: ['nuxt-pglite'],

  pglite: {
    client: {
      options: {
        dataDir: 'idb://nuxt-pglite',
      },
    },
    server: {
      options: {
        dataDir: './database/pglite', // will use `~~/server/database/pglite`
      },
    },
  },
})

For supported filesystem please refer to the official documentation.

Extensions

Extensions are automatically configured with full type support and can be added via nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['nuxt-pglite'],

  pglite: {
    client: {
      extensions: ['live', 'electricSync'],
    },
  },
})

For a full list of available extensions please refer to the official docs. If a new extension is missing feel free to open up a new PR by adding it to this file. I do plan to support only official and contrib extensions.

Warning

Auto configuration for server-side extensions will be supported once Nuxt v3.15 gets released. See below how to use hooks to add extensions server-side.

Live Queries

With Live Queries we can subscrive to events happening in the database and reactively update the user interface. This becomes particularly usefuly client-side thanks to Web Workers, allowing us to keep content in sync even when the user opens up multiple tabs.

To get started simply add live extension to your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['nuxt-pglite'],

  pglite: {
    client: {
      extensions: [
        // ...
        'live',
      ],
    },
  },
})

This will enable auto-import for useLiveQuery and useLiveIncrementalQuery. The quick implementation would be:

<script setup lang="ts">
const maxNumber = ref(100)
const items = useLiveQuery.sql`
  SELECT *
  FROM my_table
  WHERE number <= ${maxNumber.value}
  ORDER BY number;
`
</script>

Live queries are currently a port of the upstream implementation, you can read more here.

Hooks

We can use hooks to customize or extend PGlite at runtime. This becomes particularly useful in conjunction with RLS or adding custom extensions server-side.

RLS

PGlite supports RLS out of the box, but being a single-user/single-connection database it is more frequent to be used only client side. Lets take in example a basic implementation with nuxt-auth-utils. We'll need to create a client-only Nuxt plugin /plugins/rls.client.ts:

export default defineNuxtPlugin((nuxtApp) => {
  const { user } = useUserSession()

  if (user) {
    nuxtApp.hook('pglite:config', (options) => {
      options.username = user.id
    })
  }
})

This, in combination with Sync, will make us able to create an offline-first application with the ability for the users to save their data in a centralized postgres instance.

Customizing extensions

We can also use hooks to pass custom options to extensions like Sync as well as improve typing for the whole project.

In the following example we are creating a /server/plugins/extend-pglite.ts plugin that adds and configure pgvector and Sync:

import { vector } from '@electric-sql/pglite/vector'
import { electricSync } from '@electric-sql/pglite-sync'

import { pgliteHooks } from '#pglite-utils'

export default defineNitroPlugin(() => {
  pgliteHooks.hook('pglite:config', (options) => {
    options.extensions = {
      vector,
      electric: electricSync({
        metadataSchema: 'my-electric',
      }),
    }
  })

  pgliteHooks.hookOnce('pglite', async (pg) => {
    await pg.query('CREATE EXTENSION IF NOT EXISTS vector;')
  })
})

declare module '#pglite-utils' {
  interface PGliteServerExtensions {
    vector: typeof vector
    electric: ReturnType<typeof electricSync>
  }
}

Warning

Until Nuxt v3.15 gets released this is the only way to add extensions server-side.

Hooking Notes

A few things to consider are that:

  • we rely on nuxtApp hooks for client-side, while pgliteHooks imported from #pglite-utils for server-side, hooks available are:
    • pglite:config: provides access to PGliteOptions before initializing a new PGlite instance.
    • pglite: called on every PGlite execution.
  • To improve types when manually adding extensions we use PGliteClientExtensions and PGliteServerExtensions for client and server respectively.

ORM support

Any ORM that accept a PGlite instance should be supported both server and client side.

Drizzle

Drizzle integration is as simple as:

import { drizzle } from 'drizzle-orm/pglite'
import * as schema from '../my-path-to/schema'

export function useDB() {
  return drizzle(usePGlite(), { schema })
}

Contribution

Local development
# Install dependencies
pnpm install

# Generate type stubs
pnpm run dev:prepare

# Develop with the playground
pnpm run dev

# Build the playground
pnpm run dev:build

# Run ESLint
pnpm run lint

# Run Vitest
pnpm run test
pnpm run test:watch

# Release new version
pnpm run release

License

Published under the MIT license.