File: README.md
PGlite - Postgres in WASM
PGlite is a WASM Postgres build packaged into a TypeScript client library that enables you to run Postgres in the browser, Node.js, Bun and Deno, with no need to install any other dependencies. It is only 3mb gzipped and has support for many Postgres extensions, including pgvector and PostGIS.
import { PGlite } from "@electric-sql/pglite";
const db = new PGlite();
await db.query("select 'Hello world' as message;");
// -> { rows: [ { message: "Hello world" } ] }It can be used as an ephemeral in-memory database, or with persistence either to the file system (Node/Bun/Deno) or indexedDB (Browser).
Unlike previous "Postgres in the browser" projects, PGlite does not use a Linux virtual machine - it is simply Postgres in WASM.
For full documentation and user guides see pglite.dev.
Browser
It can be installed and imported using your usual package manager:
import { PGlite } from "@electric-sql/pglite";or using a CDN such as JSDeliver:
import { PGlite } from "https://cdn.jsdelivr.net/npm/@electric-sql/pglite/dist/index.js";Then for an in-memory Postgres:
const db = new PGlite()
await db.query("select 'Hello world' as message;")
// -> { rows: [ { message: "Hello world" } ] }or to persist the database to indexedDB:
const db = new PGlite("idb://my-pgdata");Node/Bun/Deno
Install into your project:
NodeJS
npm install @electric-sql/pgliteBun
bun install @electric-sql/pgliteDeno
deno add npm:@electric-sql/pgliteTo use the in-memory Postgres:
import { PGlite } from "@electric-sql/pglite";
const db = new PGlite();
await db.query("select 'Hello world' as message;");
// -> { rows: [ { message: "Hello world" } ] }or to persist to the filesystem:
const db = new PGlite("./path/to/pgdata");How it works
PostgreSQL typically operates using a process forking model; whenever a client initiates a connection, a new process is forked to manage that connection. However, programs compiled with Emscripten - a C to WebAssembly (WASM) compiler - cannot fork new processes, and operates strictly in a single-process mode. As a result, PostgreSQL cannot be directly compiled to WASM for conventional operation.
Fortunately, PostgreSQL includes a "single user mode" primarily intended for command-line usage during bootstrapping and recovery procedures. Building upon this capability, PGlite introduces an input/output pathway that facilitates interaction with PostgreSQL when it is compiled to WASM within a JavaScript environment.
Limitations
- PGlite is single user/connection.
How to build PGlite and contribute
The build process of PGlite is split into two parts:
- Building the Postgres WASM module.
- Building the PGlite client library and other TypeScript packages.
Docker is required to build the WASM module, along with Node (v20 or above) and pnpm for package management and building the TypeScript packages.
To start checkout the repository and install dependencies:
git clone --recurse-submodules https://github.com/electric-sql/pglite
cd pglite
pnpm installTo build everything, we have the convenient pnpm build:all command in the root of the repository. This command will:
- Use Docker to build the Postgres WASM module. The artifacts produced by this step are then copied to
/packages/pglite/release. - Build the PGlite client library and other TypeScript packages.
To only build the Postgres WASM module (i.e. point 1 above), run
pnpm wasm:buildIf you don't want to build the WASM module and assorted WASM binaries from scratch, they are generated automatically on Github after each successful PR merge. You can download the latest binaries by going to the last successfully merged PR and clicking the link after the comment Interim build files:. Extract the files and place them under packages/pglite/release in your local repo copy.
To build all TypeScript packages (i.e. point 2 of the above), run:
pnpm ts:buildThis will build all packages in the correct order based on their dependency relationships. You can now develop any individual package using the build and test scripts, as well as the stylecheck and typecheck scripts to ensure style and type validity.
Or alternatively to build a single package, move into the package directory and run:
cd packages/pglite
pnpm buildWhen ready to open a PR, run the following command at the root of the repository:
pnpm changesetAnd follow the instructions to create an appropriate changeset. Please ensure any contributions that touch code are accompanied by a changeset.
Acknowledgments
PGlite builds on the work of Stas Kelvich of Neon in this Postgres fork.
Sponsors
Big shoutout to everybody supporting us!
Blacksmith
License
PGlite is dual-licensed under the terms of theย Apache License 2.0ย and theย PostgreSQL License, you can choose which you prefer.
Changes to theย Postgres sourceย are licensed under the PostgreSQL License.
File: docs/docs/framework-hooks/react.md
outline: [2, 3]
React
To aid integration of PGlite into a React project we have a PGliteProvider with a corresponding usePGlite and hooks for the live query plugin.
PGliteProvider
The PGliteProvider enables you to initiate a PGlite database and pass it to all child components for use with the usePGlite, useLiveQuery, and useLiveIncrementalQuery hooks.
To use it, pass a PGlite instance as the db property.
import { PGlite } from "@electric-sql/pglite"
import { live } from "@electric-sql/pglite/live"
import { PGliteProvider } from "@electric-sql/pglite-react"
const db = await PGlite.create({
extensions: { live }
})
const App = () => {
// ...
return (
// ...
)
}usePGlite
You can retrieve the provided PGlite instance using usePGlite and then query it from within your components.
import { usePGlite } from "@electric-sql/pglite-react"
const MyComponent = () => {
const db = usePGlite()
const insertItem = () => {
db.query("INSERT INTO my_table (name, number) VALUES ('Arthur', 42);")
}
return (
<>
)
}makePGliteProvider
The makePGliteProvider function returns a PGliteProvider component and a usePGlite hook with the specified type, which enables you to provide a PGlite instance with all added extensions and retain then namespaces and types added to it.
import { PGlite, PGliteInterfaceExtensions } from '@electric-sql/pglite'
import { LiveNamespace } from '@electric-sql/pglite/live'
import { VectorNamespace } from '@electric-sql/pglite-pgvector'
import { makePGliteProvider } from '@electric-sql/pglite-react'
const { PGliteProvider, usePGlite } = makePGliteProvider<
PGlite &
PGliteInterfaceExtensions<{
live: typeof live
vector: typeof vector
}>
>()
export { PGliteProvider, usePGlite }useLiveQuery
The useLiveQuery hook enables you to reactively re-render your component whenever the results of a live query change. It wraps the .live.query() API.
It has the interface:
function useLiveQuery<T = { [key: string]: unknown }>(
query: string,
params: unknown[] | undefined | null,
): Results<T>And its arguments are:
- the SQL query
- optional parameters for the query
import { useLiveQuery } from '@electric-sql/pglite-react'
const MyComponent = () => {
const maxNumber = 100
const items = useLiveQuery(`
SELECT *
FROM my_table
WHERE number <= $1
ORDER BY number;
`, [maxNumber])
return (
<>
{
items.map((item) =>
<MyItem item={item} />
)
}
</>
)
}useLiveQuery.sql
Similarly to how you can use [ sql``](../api.md#sql) to to construct SQL queries through string templating, you can do that with useLiveQuery.sql`` for the hook equivalent. See the templating section of the API for more details.
import { useLiveQuery } from '@electric-sql/pglite-react'
const MyComponent = () => {
const maxNumber = 100
const items = useLiveQuery.sql`
SELECT *
FROM my_table
WHERE number <= ${maxNumber}
ORDER BY number;
`
// ...useLiveIncrementalQuery
The useLiveIncrementalQuery hook enables you to reactively re-render your component whenever the results of a live query change. It wraps the .live.incrementalQuery() API, which provides a way to efficiently diff the query results in Postgres.
It has the interface:
function useLiveIncrementalQuery<T = { [key: string]: unknown }>(
query: string,
params: unknown[] | undefined | null,
key: string,
): Results<T>And its arguments are:
- the SQL query
- optional parameters for the query
- the name of the column to key the diff algorithm on
import { useLiveIncrementalQuery } from '@electric-sql/pglite-react'
const MyComponent = () => {
const maxNumber = 100
const items = useLiveIncrementalQuery(`
SELECT *
FROM my_table
WHERE number <= $1
ORDER BY number;
`, [maxNumber], 'id')
return (
<>
{
items.map((item) =>
<MyItem item={item} />
)
}
</>
)
}File: docs/docs/framework-hooks/vue.md
outline: [2, 3]
Vue
To aid integration of PGlite into a Vue project we have a providePGlite with a corresponding injectPGlite and hooks for the live query plugin.
providePGlite
The providePGlite API, which follows the Vue provide / inject pattern, enables you to initiate a PGlite database and pass it to all child components for use with the corresponding injectPGlite method, as well as with the useLiveQuery and useLiveIncrementalQuery hooks.
To use it, pass a PGlite instance as the db property.
<script lang="ts" setup>
import { PGlite } from '@electric-sql/pglite'
import { providePGlite } from '@electric-sql/pglite-vue'
const db = new PGlite()
providePGlite(db)
</script>
// ...injectPGlite
You can retrieve the provided PGlite instance using injectPGlite and then query it from within your components.
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue'
import { injectPGlite } from '@electric-sql/pglite-vue'
const db = injectPGlite()
const insertItem = () => {
db.query("INSERT INTO my_table (name, number) VALUES ('Arthur', 42);")
}
</script>
<template>
// ...
Insert item
// ...
</template>makePGliteDependencyInjector
The makePGliteDependencyInjector function returns typed versions of providePGlite and injectPGlite, which enables you to provide a PGlite instance with all added extensions and retain then namespaces and types added to it.
import { PGlite, PGliteInterfaceExtensions } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
import { vector } from '@electric-sql/pglite-pgvector'
import { makePGliteDependencyInjector } from '@electric-sql/pglite-vue'
const { providePGlite, injectPGlite } = makePGliteDependencyInjector<
PGlite &
PGliteInterfaceExtensions<{
live: typeof live
vector: typeof vector
}>
>()
export { providePGlite, injectPGlite }useLiveQuery
The useLiveQuery hook enables you to reactively receive updates to the results of a live query change. It wraps the .live.query() API.
It has the interface:
function useLiveQuery<T = { [key: string]: unknown }>(
query: string | WatchSource<string>,
params?: QueryParams | WatchSource<QueryParams> | WatchSource[],
): LiveQueryResults<T>And its arguments, which can also be watch sources that will trigger a re-run, are:
- the SQL query
- optional parameters for the query
<script lang="ts">
import { useLiveQuery } from '@electric-sql/pglite-vue'
const maxNumber = 100
const items = useLiveQuery(
`
SELECT *
FROM my_table
WHERE number <= $1
ORDER BY number;
`,
[maxNumber],
)
</script>
<template>
<MyItem v-for="item in items" :item="item" :key="item.id" />
</template>useLiveQuery.sql
Similarly to how you can use [ sql``](../api.md#sql) to to construct SQL queries through string templating, you can do that with useLiveQuery.sql`` for the hook equivalent. See the templating section of the API for more details.
<script lang="ts">
import { useLiveQuery } from '@electric-sql/pglite-vue'
const maxNumber = 100
const items = useLiveQuery.sql`
SELECT *
FROM my_table
WHERE number <= ${maxNumber}
ORDER BY number;
`
</script>
// ...useLiveIncrementalQuery
The useLiveIncrementalQuery hook enables you to reactively receive updates whenever the results of a live query change. It wraps the .live.incrementalQuery() API, which provides a way to efficiently diff the query results in Postgres.
It has the interface:
export function useLiveIncrementalQuery<T = { [key: string]: unknown }>(
query: string | WatchSource<string>,
params: QueryParams | WatchSource<QueryParams> | WatchSource[],
key: string | WatchSource<string>,
): LiveQueryResults<T>And its arguments, which can also be watch sources that will trigger a re-run, are:
- the SQL query
- optional parameters for the query
- the name of the column to key the diff algorithm on
<script lang="ts">
import { useLiveIncrementalQuery } from '@electric-sql/pglite-vue'
const maxNumber = 100
const items = useLiveIncrementalQuery(
`
SELECT *
FROM my_table
WHERE number <= $1
ORDER BY number;
`,
[maxNumber],
'id',
)
</script>
<template>
<MyItem v-for="item in items" :item="item" :key="item.id" />
</template>File: docs/docs/about.md
What is PGlite
PGlite is a WASM Postgres build packaged into a TypeScript/JavaScript client library, that enables you to run Postgres in the browser, Node.js and Bun, with no need to install any other dependencies. It's under 3mb Gzipped, and has support for many Postgres extensions, including pgvector.
Getting started with PGlite is simple: just install and import the NPM package, then create your embedded database:
import { PGlite } from '@electric-sql/pglite'
const db = new PGlite()
await db.query("select 'Hello world' as message;")
// -> { rows: [ { message: "Hello world" } ] }It can be used as an ephemeral in-memory database, or with persistence either to the file system (Node/Bun), or IndexedDB (browser).
Unlike previous "Postgres in the browser" projects, PGlite does not use a Linux virtual machine - it is simply Postgres in WASM.
It's being developed by Electric for our use case of embedding into applications, either locally or at the edge, allowing users to sync a subset of their server-side Postgres database.
However, there are many more use cases for PGlite beyond its use as an embedded application database:
Unit and CI testing
PGlite is very fast to start and tear down. It's perfect for unit tests - you can have a unique fresh Postgres for each test.Local development
You can use PGlite as an alternative to a full local Postgres for development; simplifying your development environments.Remote development, or local web containers
As PGlite is so lightweight it can be easily embedded into remote containerised development environments, or in-browser web containers.On-device or edge AI and RAG
PGlite has full support for pgvector, enabling a local or edge retrieval augmented generation (RAG) workflow.
We are very keen to establish PGlite both as an open source, and open contribution, project, working to build a community around it, so as to develop its capabilities for all use cases.
Read more in our getting started guide.
File: docs/docs/api.md
outline: [2, 3]
PGlite API
Main Constructor
The main constructor is imported as:
import { PGlite } from '@electric-sql/pglite'The preferred way to create a PGlite instance is with the PGlite.create() static method that returns a promise, resolving to the new PGlite instance.
await PGlite.create(dataDir: string, options: PGliteOptions)await PGlite.create(options: PGliteOptions)
There are a couple of advantages to using the static method:
- This awaits the
.waitReadypromise, ensuring that the database has been fully initialised. - When using TypeScript and extensions, the returned PGlite instance will have the extensions namespace on its type. This is not possible with the standard constructor due to TypesScript limitations.
A new PGlite instance can also be created using the new PGlite() constructor.
new PGlite(dataDir: string, options: PGliteOptions)new PGlite(options: PGliteOptions)
`dataDir`
Path to the directory for storing the Postgres database. You can provide a URI scheme for various storage backends:
file://or unprefixed
File system storage, available in Node and Bun.idb://
IndexedDB storage, available in the browser.memory://
In-memory ephemeral storage, available in all platforms.
`options`
dataDir?: string
The directory in which to store the Postgres database when not provided as the first argument.debug?: 1-5
the Postgres debug level. Logs are sent to the console.relaxedDurability?: boolean
Under relaxed durability mode, PGlite will not wait for flushes to storage to complete after each query before returning results. This is particularly useful when using the IndexedDB file system.fs?: Filesystem
The alternative to providing a dataDir with a filesystem prefix is to initialise aFilesystemyourself and provide it here. See FilesystemsloadDataDir?: Blob | File
A tarball of a PGlitedatadirto load when the database starts. This should be a tarball produced from the related.dumpDataDir()method.extensions?: Extensions
An object containing the extensions you wish to load.username?: string
The username of the user to connect to the database as. Permissions will be applied in the context of this user.database?: string
The database from the Postgres cluster within thedataDirto connect to.initialMemory?: number
The initial amount of memory in bytes to allocate for the PGlite instance. PGlite will grow the memory automatically, but if you have a particularly large database you can set this higher to prevent the pause during memory growth.pgliteWasmModule?: WebAssembly.Module
A precompiled pglite WASM module to use instead of downloading the default version, or when using a bundler that either can, or requires, loading the WASM module with a ESM import.initdbWasmModule?: WebAssembly.Module
A precompiled initdb WASM module to use instead of downloading the default version, or when using a bundler that either can, or requires, loading the WASM module with a ESM import.fsBundle?: Blob | File
A filesystem bundle to use instead of downloading the default version. This is useful if in a restricted environment such as an edge worker.parsers: ParserOptions
An object of type{ [pgType: number]: (value: string) => any; }mapping Postgres data type IDs to parser functions. For convenience, thepglitepackage exports a constant for most common Postgres types.tsimport { PGlite, types } from '@electric-sql/pglite' const pg = await PGlite.create({ parsers: { [types.TEXT]: (value) => value.toUpperCase(), }, })serializers: SerializerOptions
An object of type{ [pgType: number]: (value: any) => string; }mapping Postgres data type IDs to serializer functions.tsimport { PGlite, types } from '@electric-sql/pglite' const pg = await PGlite.create({ serializers: { [types.NUMERIC]: (value) => value.toString(), }, })startParams?: string[]
An array of strings that will be passed to the Postgres process as start parameters. This is the set of parameters one would pass to a native PostgreSQL instance.tsimport { PGlite } from '@electric-sql/pglite' const pg = await PGlite.create({ startParams: [ ...PGlite.defaultStartParams, '-c', 'application_name=My awesome backend', ], })initDbStartParams?: string[]
An array of strings that will be passed to the initdb process as start parameters. This is the set of parameters one would pass to a native initdb exe.tsimport { PGlite } from '@electric-sql/pglite' const pg = await PGlite.create({ initDbStartParams: ['--locale-provider=icu', '--icu-locale=de'], })postgresqlconf?: string[] | string
Apostgresql.conffile as an array of strings or a single string. You can use it to pass parameters to the PostgreSQL process. See official documentation for details.
`options.extensions`
PGlite and Postgres extensions are loaded into a PGLite instance on start, and can include both a WASM build of a Postgres extension and/or a PGlite client plugin.
The options.extensions parameter is an object of namespace: extension parings. The namespace is used to expose the PGlite client plugin included in the extension. An example of this is the live queries extension.
import { PGlite } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
import { vector } from '@electric-sql/pglite-pgvector'
const pg = await PGlite.create({
extensions: {
live, // Live query extension, is a PGlite client plugin
vector, // Postgres pgvector extension
},
})
// The `live` namespace is added by the use of the
// `live` key in the `extensions` object.
pg.live.query('...')For information on how to develop a PGlite extension see Extension Development.
Methods
query
.query<T>(query: string, params?: any[], options?: QueryOptions): Promise<Results<T>>
Execute a single statement, optionally with parameters.
Uses the extended query Postgres wire protocol.
Returns single result object.
Example
await pg.query('INSERT INTO test (name) VALUES ($1);', ['test'])
// { affectedRows: 1 },Query Options
The query and exec methods take an optional options objects with the following parameters:
rowMode: "object" | "array"
The returned row object type, either an object offieldName: valuemappings or an array of positional values. Defaults to"object".parsers: ParserOptions
An object mapping Postgres data type IDs to parser functions. This option overrides any parsers set at the instance level.tsimport { types } from '@electric-sql/pglite' await pg.query(`SELECT * FROM test WHERE name = $1;`, ['test'], { parsers: { [types.TEXT]: (value) => value.toUpperCase(), }, })serializers: SerializerOptions
An object mapping Postgres data type IDs to serializer functions. This option overrides any serializers set at the instance level.tsimport { types } from '@electric-sql/pglite' await pg.query(`INSERT INTO test (numeric) VALUES ($1);`, [100n], { serializers: { [types.NUMERIC]: (value: number | bigint) => value.toString(), }, })blob: Blob | File
Attach aBloborFileobject to the query that can used with aCOPY FROMcommand by using the virtual/dev/blobdevice, see importing and exporting.
sql
.sql<T>``: Promise<Results<T>>
.sql<T>(strings: TemplateStringsArray, ...params: any[]): Promise<Results<T>>
You can also use the [ sql``](#tagged-template-queries) tagged template literal to create queries:
await pg.sql`SELECT * FROM test WHERE name = ${'test'}`
// equivalent of pg.query('SELECT * FROM test WHERE name = $1', ['test'])See the templating section for more details on how to use the tagged template literal along with various helpers for more complex cases.
exec
.exec(query: string, options?: QueryOptions): Promise<Array<Results>>
Execute one or more statements. (note that parameters are not supported)
This is useful for applying database migrations, or running multi-statement SQL that doesn't use parameters.
Uses the simple query Postgres wire protocol.
Returns array of result objects; one for each statement.
Example
await pg.exec(`
CREATE TABLE IF NOT EXISTS test (
id SERIAL PRIMARY KEY,
name TEXT
);
INSERT INTO test (name) VALUES ('test');
SELECT * FROM test;
`)
// [
// { affectedRows: 0 },
// { affectedRows: 1 },
// {
// rows: [
// { id: 1, name: 'test' }
// ]
// affectedRows: 0,
// fields: [
// { name: 'id', dataTypeID: '23' },
// { name: 'name', dataTypeID: '25' },
// ]
// }
// ]transaction
.transaction<T>(callback: (tx: Transaction) => Promise<T>)
To start an interactive transaction, pass a callback to the transaction method. It is passed a Transaction object which can be used to perform operations within the transaction.
The transaction will be committed when the promise returned from your callback resolves, and automatically rolled back if the promise is rejected.
`Transaction` objects
tx.query<T>(query: string, params?: any[], options?: QueryOptions): Promise<Results<T>>
The same as the main.querymethod.tx.sql<T>``: Promise<Results<T>>
The same as the main.sqltemplate string method.tx.exec(query: string, options?: QueryOptions): Promise<Array<Results>>
The same as the main.execmethod.tx.rollback()
Rollback and close the current transaction.
Example
await pg.transaction(async (tx) => {
await tx.query(
'INSERT INTO test (name) VALUES ('$1');',
[ 'test' ]
);
return await ts.query('SELECT * FROM test;');
});close
.close(): Promise<void>
Close the database, ensuring it is shut down cleanly.
listen
.listen(channel: string, callback: (payload: string) => void): Promise<(() => Promise<void>>
Subscribe to a pg_notify channel. The callback will receive the payload from the notification.
Returns an unsubscribe function to unsubscribe from the channel.
Example
const unsub = await pg.listen('test', (payload) => {
console.log('Received:', payload)
})
await pg.query("NOTIFY test, 'Hello, world!'")Channel names are case sensitive if double-quoted (pg.listen('"TeST"')). Otherwise channel name will be lower cased (pg.listen('TeStiNG') == pg.listen('testing')).
unlisten
.unlisten(channel: string, callback?: (payload: string) => void): Promise<void>
Unsubscribe from the channel. If a callback is provided it removes only that callback from the subscription. When no callback is provided, it unsubscribes all callbacks for the channel.
onNotification
onNotification(callback: (channel: string, payload: string) => void): () => void
Add an event handler for all notifications received from Postgres.
Note: This does not subscribe to the notification; you will need to manually subscribe with LISTEN channel_name.
offNotification
offNotification(callback: (channel: string, payload: string) => void): void
Remove an event handler for all notifications received from Postgres.
dumpDataDir
dumpDataDir(compression?: 'auto' | 'gzip' | 'none'): Promise<File | Blob>
Dump the Postgres datadir to a Gzipped tarball.
The compression option defaults to auto which uses compression where possible. You can explicit opt in or out of compression with gzip and none. When you specify that compression is required with gzip, if the environment doesn't support a suitable compression API it will throw an error.
This can then be used in combination with the loadDataDir option when starting PGlite to load a dumped database from storage.
::: tip NOTE
The datadir dump may not be compatible with other Postgres versions; it is only designed for importing back into PGlite.
:::
execProtocol
execProtocol(message: Uint8Array, options?: ExecProtocolOptions): Promise<Array<[BackendMessage, Uint8Array]>>
Execute a Postgres wire protocol message, returning an array of tuples, one for each wire protocol result message, consisting of:
- The passed message object - see pg-protocol
- The raw
Uint8Arrayfor that message.
This API is safe to use alongside the other PGlite query APIs as it handles error, transactions and notifications.
execProtocolRaw
execProtocolRaw(message: Uint8Array, options?: ExecProtocolOptions): Promise
Execute a Postgres wire protocol message, returning the unparsed result Uint8Array, this includes all wire protocol result messages emitted as a result of your message and will require external passing. This is the lowest level API exposed by PGlite and can be used to interact with a PGlite database using existing Postgres clients. It is likely that you will want to use something such as pg-gateway that uses this internally to expose the database on a TCP socket.
::: warning WARNING
execProtocolRaw bypasses PGlite's protocol wrappers that manage error/notice messages,
transactions, and notification listeners. Only use if you need to bypass these wrappers and don't intend to use the above features. execProtocol is a safer alternative.
:::
execProtocolRawStream(message: Uint8Array, options: ExecProtocolOptionsStream): void
Same as execProtocolRaw but returns raw messages in the options.onRawData callback as they arrive from the backend. This is particularly useful when expecting large sets of data.
See a usage example in the pglite-socket project, where the server uses this method to pass the results to the client socket as they arrive.
describeQuery
.describeQuery(query: string, options?: QueryOptions): Promise<DescribeQueryResult>
Get type information about a query's parameters and result fields without executing it. This is useful for understanding the expected parameter types and the structure of the results that would be returned. It can be used to build a type inference system for queries using PGlite.
Returns an object with:
queryParams: Array<{ dataTypeID: number, serializer: Function }>
Information about each parameter's Postgres type ID and the serializer function used to convert JavaScript values to Postgres format.resultFields: Array<{ name: string, dataTypeID: number, parser: Function }>
Information about each result field including its name, Postgres type ID, and the parser function used to convert Postgres values to JavaScript format.
refreshArrayTypes
.refreshArrayTypes(): Promise<void>
Refresh the array types in the database. This is useful when you have added columns that contain array types (ie. array of enums) and need to update the internal array type cache. This is done automatically when the database is started, but can be called manually if needed.
Example
await pg.describeQuery('SELECT * FROM test WHERE name = $1', ['test'])
// returns:
{
queryParams: [{ dataTypeID: 25, serializer: Function }],
resultFields: [{ name: "id", dataTypeID: 23, parser: Function }],
}clone
.clone(): Promise
Clones the current instance. This is useful when a series of operations, like unit or integration test, need to be run on the same database without having to recreate the database each time, or for each test.
copyToFS
.copyToFS(filePath: string, data: Uint8Array, mode?: number)
Copies the data to a file at filePath in the virtual filesystem and sets the optional mode of the file.
Example
await pg.copyToFS(
'/tmp/test.txt',
new TextEncoder().encode('PGlite says hi!'),
0o0644,
)Properties
ready
.ready boolean (read only)
Whether the database is ready to accept queries.
closed
.closed boolean (read only)
Whether the database is closed and no longer accepting queries.
waitReady
.waitReady Promise<void>
Promise that resolves when the database is ready to use.
::: tip NOTE
Query methods will wait for the waitReady promise to resolve if called before the database has fully initialised, and so it is not necessary to wait for it explicitly.
:::
`Results` Objects
Result objects have the following properties:
rows: Row<T>[]
The rows returned by the query.affectedRows?: number
Count of the rows affected by the query. Note, this is not the count of rows returned, it is the number or rows in the database changed by the query.command?: string
The command of the query that was run, taken from the command tag reported by Postgres, e.g.SELECT,INSERTorCREATE. Matches thecommandfield of node-postgres query results.rowCount?: number
The number of rows reported by the command tag, e.g. the rows returned by aSELECTor changed by anUPDATE. Not set when the tag carries no count (e.g.CREATE TABLE). UnlikeaffectedRowsthis is per statement and not cumulative; it matches therowCountfield of node-postgres query results.fields: { name: string; dataTypeID: number }[]
Field name and Postgres data type ID for each field returned.blob?: Blob
ABlobcontaining the data written to the virtual/dev/blob/device by aCOPY TOcommand. See /dev/blob.
`Row` Objects
Rows objects are a key / value mapping for each row returned by the query.
The .query<T>() method can take a TypeScript type describing the expected shape of the returned rows.
::: tip NOTE
These types are not validated at run time, the result is only cast to the provided type.
:::
Tagged Template Queries
PGlite has support for using tagged template literals to construct SQL queries via the .sql`` method on both the main PGlite instance and Transaction objects. Substituted template values are automatically converted to query parameters, and you receive the same results as calling the query API directly.
const name = getName()
await pg.sql`SELECT * FROM test WHERE name = ${name}`There are helpers in the template export to create more complex and parametrizeable templated queries:
identifier``
Tag identifiers like column and table names to escape them with quotes and prevent them from becoming parameters.raw``
Tag any string that you want to avoid being interpreted as a parameter. Will allow you to still do regular string templating.sql``
Tag nested templated literals to preserve behaviour and parametrization. Will allow you to create reusable templating utilitiesquery``
Use at the top level tag in order to generate the parametrized query without passing it to thequeryAPI. Returns a{ query: string, params: any[] }object with the parametrized query and any parameters used in the query.
If you require additional configurations or complex binary parameters it's best to use the query method.
Example
import { identifier, raw, sql, query } from '@electric-sql/pglite/template'
await pg.sql`SELECT * FROM ${identifier`test`} WHERE name = ${'test'}`
// equivalent of pg.query('SELECT * FROM "test" WHERE name = $1', ['test'])
const filterStmt = (filterVar?: string) =>
filterVar ? sql`WHERE name = ${filterVar}` : raw`WHERE 1=1`
await pg.sql`SELECT * FROM test ${filterStmt('test')}`
// equivalent of pg.query('SELECT * FROM "test" WHERE name = $1', ['test'])
await pg.sql`SELECT * FROM test ${filterStmt(null)}`
// equivalent of pg.query('SELECT * FROM "test" WHERE 1=1, [])
query`SELECT * FROM ${identifier`test`} WHERE name = ${'test'}`
// { query: 'SELECT * FROM "test" WHERE name = $1', params: ['test'] }/dev/blob
PGlite has support for importing and exporting via the SQL COPY TO/FROM command by using a virtual /dev/blob device.
To import a file, pass the File or Blob in the query options as blob, and copy from the /dev/blob device.
await pg.query("COPY my_table FROM '/dev/blob';", [], {
blob: MyBlob,
})To export a table or query to a file, you just need to write to the /dev/blob device; the file will be returned as blob on the query results:
const ret = await pg.query("COPY my_table TO '/dev/blob';")
// ret.blob is a `Blob` object with the data from the copy.File: docs/docs/bundler-support.md
Bundler Support
Some bundlers require additional configuration to work with PGlite.
:::tip
If you come across any issues with PGlite and a specific bundler, please open an issue, we'd also love any contributions to this bundler documentation if you're able to help out.
:::
Vite
When using Vite, make sure to exclude pglite from dependency optimization using the optimizeDeps option inside vite.config.js:
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
exclude: ['@electric-sql/pglite'],
},
})Additional configuration for the Multi-tab Worker
When using the Multi-tab Worker, you might encounter errors during a production build related to workers being bundle in iife format, to resolve this modify the worker.format option in vite.config.js to 'es' (the default is 'iife')
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: {
exclude: ['@electric-sql/pglite'],
},
worker: {
format: 'es',
},
})When importing the worker in your script, you can use the recommended ?worker import method from Vite:
import PGWorker from './worker.js?worker'
export const pglite = new PGliteWorker(
new PGWorker({
type: 'module',
name: 'pglite-worker',
}),
{
// ...your options here
}
},
)esbuild
esbuild does not support new URL('./file', import.meta.url) pattern that PGlite uses to locate its WebAssembly and data files. This means the automatic file resolution won't work out of the box.
Workaround: manually provide `pgliteWasmModule`, `initdbWasmModule` and `fsBundle`
Copy
pglite.wasm,initdb.wasmandpglite.datafromnode_modules/@electric-sql/pglite/dist/to your public/build directory so your web server can serve them.Pass them manually when creating a PGlite instance:
import { PGlite } from '@electric-sql/pglite'
const [pgliteWasmModule, initdbWasmModule, fsBundle] = await Promise.all([
WebAssembly.compileStreaming(fetch('/pglite.wasm')),
WebAssembly.compileStreaming(fetch('/initdb.wasm')),
fetch('/pglite.data').then((response) => response.blob()),
])
const db = await PGlite.create({
pgliteWasmModule,
initdbWasmModule,
fsBundle,
})Alternatively, you can use an esbuild plugin like @chialab/esbuild-plugin-meta-url to handle new URL() imports automatically.
Next.js
When using Next.js, make sure to add @electric-sql/pglite to the transpilePackages array in next.config.js:
const nextConfig = {
swcMinify: false,
transpilePackages: [
'@electric-sql/pglite-react', // Optional
'@electric-sql/pglite',
],
}
export default nextConfigFile: docs/docs/filesystems.md
Filesystems
PGlite has a virtual file system layer that allows it to run in environments that don't traditionally have filesystem access.
PGlite VFSs are under active development, and we plan to extend the range of options in future, as well as make it easy for users to create their own filesystems.
We would recommend using the IndexedDB VFS in the browser at the current time as the OPFS VFS is not supported by Safari.
In-memory FS
The in-memory FS is the default when starting PGlite, and it is available on all platforms. All files are kept in memory and there is no persistence, other than calling pg.dumpDataDir() and then using the loadDataDir option at start.
To use the in-memory FS you can use one of these methods:
- Don't provide a
dataDiroptiontsconst pg = new PGlite() - Set the
dataDirtomemory://tsconst pg = new PGlite('memory://') - Import and pass the FS explicitlyts
import { MemoryFS } from '@electric-sql/pglite' const pg = new PGlite({ fs: new MemoryFS(), })
Platform Support
| Node | Bun | Deno | Chrome | Safari | Firefox |
|---|---|---|---|---|---|
| โ | โ | โ | โ | โ | โ |
Node FS
The Node FS uses the Node.js file system API to implement a VFS for PGLite. It is available in both Node and Bun.
To use the Node FS you can use one of these methods:
- Set the
dataDirto a directory on your filesystemtsconst pg = new PGlite('./path/to/datadir/') - Import and pass the FS explicitlyts
import { NodeFS } from '@electric-sql/pglite' const pg = new PGlite({ fs: new NodeFS('./path/to/datadir/'), })
Platform Support
| Node | Bun | Deno | Chrome | Safari | Firefox |
|---|---|---|---|---|---|
| โ | โ | โ |
IndexedDB FS
The IndexedDB FS persists the database to IndexedDB in the browser. It's a layer over the in-memory filesystem, loading all files for the database into memory on start, and flushing them to IndexedDB after each query if they have changed.
To use the IndexedDB FS you can use one of these methods:
- Set the
dataDirwith aidb://prefix, the database will be stored in an IndexedDB named with the path providedtsconst pg = new PGlite('idb://my-database') - Import and pass the FS explicitlyts
import { IdbFs } from '@electric-sql/pglite' const pg = new PGlite({ fs: new IdbFs('my-database'), })
The IndexedDB filesystem works at the file level, storing whole files (Postgres has a single file per table or index) as blobs in IndexedDB. Flushing whole files can take a few milliseconds after each query. To aid in building responsive apps we provide a relaxedDurability mode that can be configured when starting PGlite. Under this mode, the results of a query are returned immediately, and the flush to IndexedDB is scheduled to occur asynchronously afterwards. Typically, this is immediately after the query returns with no delay.
Platform Support
| Node | Bun | Deno | Chrome | Safari | Firefox |
|---|---|---|---|---|---|
| โ | โ | โ |
OPFS AHP FS
The OPFS AHP filesystem is built on top of the Origin Private Filesystem in the browser and uses an "access handle pool". It is only available when PGlite is run in a Web Worker, this could be any worker you configure. We provide a Multi Tab Worker to aid in using PGlite from multiple tabs in the browser.
To use the OPFS AHP FS you can use one of these methods:
- Set the
dataDirto a directory within the origins OPFStsconst pg = new PGlite('opfs-ahp://path/to/datadir/') - Import and pass the FS explicitlyts
import { OpfsAhpFS } from '@electric-sql/pglite/opfs-ahp' const pg = new PGlite({ fs: new OpfsAhpFS('./path/to/datadir/'), })
Platform Support
| Node | Bun | Deno | Chrome | Safari | Firefox |
|---|---|---|---|---|---|
| โ | โ |
Unfortunately, Safari appears to have a limit of 252 open sync access handles, this prevents this VFS from working due to a standard Postgres install consisting of over 300 files.
What is an "access handle pool"?
The Origin Private Filesystem API provides both asynchronous and synchronous methods, but the synchronous methods are limited to read, write and flush. You are unable to traverse the filesystem or open files synchronously. PGlite is a fully synchronous WASM build of Postgres and unable to call async APIs while handling a query. While it is possible to build an async WASM Postgres using Asyncify, it adds significant overhead in both file size and performance.
To overcome these limitations, and to provide a fully synchronous file system to PGlite on top of OPFS, we use something called an "access handle pool". When you first start PGlite we open a pool of OPFS access handles with randomised file names; these are then allocated to files as needed. After each query, a pool maintenance job is scheduled that maintains its size. When you inspect the OPFS directory where the database is stored, you will not see the normal Postgres directory layout, but rather a pool of files and a state file containing the directory tree mapping along with file metadata.
The PGlite OPFS AHP FS is inspired by the wa-sqlite access handle pool file system by Roy Hashimoto.
File: docs/docs/index.md
Getting started with PGlite
PGlite can be used in both Node/Bun/Deno or the browser, and with any JavaScript framework.
Install and start in Node/Bun/Deno
Install into your project:
::: code-group
npm install @electric-sql/pglitepnpm install @electric-sql/pgliteyarn add @electric-sql/pglitebun install @electric-sql/pglitedeno add npm:@electric-sql/pglite:::
To use the in-memory Postgres:
import { PGlite } from '@electric-sql/pglite'
const db = new PGlite()or to persist to the native filesystem:
const db = new PGlite('./path/to/pgdata')Install and start in the browser
It can be installed and imported using your usual package manager:
import { PGlite } from '@electric-sql/pglite'or using a CDN such as JSDeliver:
import { PGlite } from 'https://cdn.jsdelivr.net/npm/@electric-sql/pglite/dist/index.js'Then for an in-memory Postgres:
const db = new PGlite()or to persist the database to IndexedDB:
const db = new PGlite('idb://my-pgdata')Making a query
There are two methods for querying the database, .query and .exec. The former supports parameters, while the latter supports multiple statements.
First, let's create a table and insert some test data using the .exec method:
await db.exec(`
CREATE TABLE IF NOT EXISTS todo (
id SERIAL PRIMARY KEY,
task TEXT,
done BOOLEAN DEFAULT false
);
INSERT INTO todo (task, done) VALUES ('Install PGlite from NPM', true);
INSERT INTO todo (task, done) VALUES ('Load PGlite', true);
INSERT INTO todo (task, done) VALUES ('Create a table', true);
INSERT INTO todo (task, done) VALUES ('Insert some data', true);
INSERT INTO todo (task) VALUES ('Update a task');
`)The .exec method is perfect for migrations and batch inserts with raw SQL.
Now, let's retrieve an item using .query method:
const ret = await db.query(`
SELECT * from todo WHERE id = 1;
`)
console.log(ret.rows)Output:
;[
{
id: 1,
task: 'Install PGlite from NPM',
done: false,
},
]Using parameterised queries
When working with user supplied values, it's always best to use parameterized queries; these are supported on the .query method.
We can use this to update a task:
const ret = await db.query(
'UPDATE todo SET task = $2, done = $3 WHERE id = $1',
[5, 'Update a task using parameterized queries', true],
)What next?
To learn more about querying and transactions along with the other methods and options available, you can read the main PGlite API documentation.
There is also a live-query extension that enables reactive queries to update a UI when the underlying database changes.
PGlite has a number of built-in virtual file systems to provide persistence for your database.
There are framework hooks to make working with PGlite within React and Vue much easier with less boilerplate.
For help configuring PGlite with your bundler, see the bundler support page.
As PGlite only has a single exclusive connection to the database, we provide a multi-tab worker to enable sharing a PGlite instance between multiple browser tabs.
There is a REPL component that can be easily embedded into a web-app to aid in debugging and development, or as part of a database application itself.
We maintain a list of ORMs and query builders that support PGlite.
PGlite supports both Postgres extensions and PGlite Plugins via its extensions API, and there is a list of supported extensions.
We have a page of examples that you can open to test out PGlite in the browser.
File: docs/docs/live-queries.md
Live Queries
The "live" extension enables you to subscribe to a query and receive updated results when the underlying tables change.
To use the extension, it needs to be added to the PGlite instance when creating it:
import { PGlite } from '@electric-sql/pglite'
import { live } from '@electric-sql/pglite/live'
const pg = await PGlite.create({
extensions: {
live,
},
})There are three methods on the live namespace:
live.query()for basic live queries. With less machinery in PGlite, it's quicker for small results sets and narrow rows.live.incrementalQuery()for incremental queries. It materialises the full result set on each update from only the changes emitted by thelive.changesAPI. Perfect for feeding into React, and with good performance for large result sets and wide rows.live.changes()a lower level API that emits the changes (insert/update/delete) that can then be mapped to mutations in a UI or other datastore.
live.query
live.query<T>()
This is very similar to a standard query, but takes an additional callback that receives the results whenever they change:
const ret = pg.live.query('SELECT * FROM test ORDER BY rand;', [], (res) => {
// res is the same as a standard query result object
})You can also pass an options object with offset and limit to implement efficient windowed queries:
const ret = pg.live.query({
query: 'SELECT * FROM test ORDER BY rand;',
offset: 0,
limit: 10,
callback: (res) => {
// res includes:
// - rows: the rows for the current window
// - offset: the current offset
// - limit: the current limit
// - totalCount: total number of rows in the full result set
},
})The windowed query is optimized to:
- Only transfer the rows within the requested window
- Efficiently update when rows enter/leave the window
- Track the total count separately to avoid slow COUNT(*) queries blocking updates
You can update the window position by calling refresh with new offset/limit values:
// Move to page 2
await ret.refresh(10, 10)The returned value from the call is a Promise of an object with this interface:
interface LiveQueryReturn<T> {
initialResults: Results<T> & {
offset?: number // Current offset (if windowed)
limit?: number // Current limit (if windowed)
totalCount?: number // Total row count (if windowed)
}
unsubscribe: () => Promise<void>
refresh: (options?: { offset?: number; limit?: number }) => Promise<void>
}initialResultsis the initial results set (also sent to the callback)unsubscribeallows you to unsubscribe from the live queryrefreshallows you to force a refresh of the query with the updated results sent to the callback. For windowed queries you can optionally provide new offset/limit values.
Internally it watches the tables that the query depends on, and reruns the query whenever they are changed.
live.incrementalQuery
live.incrementalQuery<T>()
Similar to above, but maintains a temporary table of the previous state inside of Postgres. When the tables it depends on change, the query is re-run and diffed with the last state. Only the changes from the last version of the query are copied from WASM into JS.
It requires an additional key argument - the name of a column (often a primary key) on which to key the diff.
const ret = pg.live.incrementalQuery(
'SELECT * FROM test ORDER BY rand;',
[],
'id',
(res) => {
// res is the same as a standard query result object
},
)The returned value is of the same type as the query method above.
live.changes
live.changes()
A lower-level API which is the backend for the incrementalQuery, it emits the changes that have occurred. It requires a key on which to compare row differences:
const ret = pg.live.changes(
'SELECT * FROM test ORDER BY rand;',
[],
'id',
(res) => {
// res is a change result object
},
)The call returns a Promise of an object defined by this interface:
interface LiveChangesReturn<T = { [key: string]: any }> {
fields: { name: string; dataTypeID: number }[]
initialChanges: Array<Change<T>>
unsubscribe: () => Promise<void>
refresh: () => Promise<void>
}The results passed to the callback are an array of Change objects:
type ChangeInsert<T> = {
__changed_columns__: string[]
__op__: 'INSERT'
__after__: number
} & T
type ChangeDelete<T> = {
__changed_columns__: string[]
__op__: 'DELETE'
__after__: undefined
} & T
type ChangeUpdate<T> = {
__changed_columns__: string[]
__op__: 'UPDATE'
__after__: number
} & T
type Change<T> = ChangeInsert<T> | ChangeDelete<T> | ChangeUpdate<T>Each Change includes the new values along with:
__changed_columns__the column names that were changed.__op__the operation that is required to update the state (INSERT,UPDATE,DELETE).__after__thekeyof the row after which this row should be positioned; it will be included in__changed_columns__if it has been changed. This allows for very efficient moves within an ordered set of results.
This API can be used to implement very efficient in-place DOM updates.
File: docs/docs/multi-tab-worker.md
Multi-tab Worker
It's likely that you will want to run PGlite in a Web Worker so that it doesn't block the main thread. Additionally, as PGlite is single connection only, you may want to proxy multiple browser tabs to a single PGlite instance.
To aid in this, we provide a PGliteWorker with the same API as the standard PGlite, and a worker wrapper that exposes a PGlite instance to other tabs.
Using PGliteWorker
First, you need to create a js file for your worker instance. You use the worker wrapper with an init option that returns a PGlite instance to start that database and expose it to all tabs:
// my-pglite-worker.js
import { PGlite } from '@electric-sql/pglite'
import { worker } from '@electric-sql/pglite/worker'
worker({
async init() {
// Create and return a PGlite instance
return new PGlite()
},
})Then connect the PGliteWorker to your new worker process in your main script:
import { PGliteWorker } from '@electric-sql/pglite/worker'
const pg = new PGliteWorker(
new Worker(new URL('./my-pglite-worker.js', import.meta.url), {
type: 'module',
}),
)
// `pg` has the same interface as a standard PGlite interfaceInternally, this starts a worker for each tab, but then runs an a election to nominate one as the leader. Only the leader then starts PGlite by calling the init function, and handles all queries. When the leader tab is closed, a new election is run, and a new PGlite instance is started.
In addition to having all the standard methods of the PGlite interface, PGliteWorker also has the following methods and properties:
onLeaderChange(callback: () => void)
This allows you to subscribe to a notification when the leader worker is changed. It returns an unsubscribe function.offLeaderChange(callback: () => void)
This allows you to unsubscribe from the leader change notification.isLeader: bool
A boolean property indicating if this instance is the leader.
Passing options to a worker
PGliteWorker takes an optional second parameter options; this can include any standard PGlite options along with these additional options:
id: string
This is an optionalidto group your PGlite workers. The leader election is run between allPGliteWorkers with the sameid.
If not provided, the url to the worker is concatenated with thedataDiroption to create an id.meta: any
Any additional metadata you would like to pass to the worker processinitfunction.
The worker() wrapper takes a single options argument, with a single init property. init is a function that takes any options passed to PGliteWorker, excluding extensions, and returns a PGlite instance. You can use the options passed to decide how to configure your instance:
// my-pglite-worker.js
import { PGlite } from '@electric-sql/pglite'
import { worker } from '@electric-sql/pglite/worker'
worker({
async init(options) {
const meta = options.meta
// Do something with additional metadata.
// or even run your own code in the leader along side the PGlite
return new PGlite({
dataDir: options.dataDir,
})
},
})
// my-app.js
import { PGliteWorker } from '@electric-sql/pglite/worker'
const pg = new PGliteWorker(
new Worker(new URL('./my-pglite-worker.js', import.meta.url), {
type: 'module',
}),
{
dataDir: 'idb://my-db',
meta: {
// additional metadata passed to `init`
},
},
)Extension support
PGliteWorker has support for both Postgres extensions and PGlite plugins using the normal extension api.
Any extension can be used by the PGlite instance inside the worker, however the extensions namespace is not exposed on a connecting PGliteWorker on the main thread.
// my-pglite-worker.js
import { PGlite } from '@electric-sql/pglite'
import { worker } from '@electric-sql/pglite/worker'
import { vector } from '@electric-sql/pglite-pgvector'
worker({
async init() {
return new PGlite({
extensions: {
vector,
},
})
},
})Extensions that only use the PGlite plugin interface, such as live queries, can be used on the main thread with PGliteWorker to expose their functionality; this is done by providing a standard options object as a second argument to the PGliteWorker constructor:
import { PGliteWorker } from '@electric-sql/pglite/worker'
import { live } from '@electric-sql/pglite/live'
const pg = new PGliteWorker(
new Worker(new URL('./my-pglite-worker.js', import.meta.url), {
type: 'module',
}),
{
extensions: {
live,
},
},
)PGliteWorker also has a create static method that resolves to a new instance when it is fully initiated. This also adds the correct types for any extensions to the PGliteWorker instance:
import { PGliteWorker } from '@electric-sql/pglite/worker'
import { live } from '@electric-sql/pglite/live'
const pg = await PGliteWorker.create(
new Worker(new URL('./my-pglite-worker.js', import.meta.url), {
type: 'module',
}),
{
extensions: {
live,
},
},
)
// TypeScript is aware of the `pg.live` namespace:
pg.live.query(/* ... */)