pglite
is a WASM build of postgres
that runs in the browser.
Using pglite
we can skip the server and instead store the data locally in the user's device. This is ideal for offline-first apps, and it offers a lot of advantages:
- No server to manage
- Complete data control and privacy for the user
- Avoid round trips to the server for better performance
- Less complexity around handling errors and connectivity issues
These are some of the same advantages that come with implementing a local-first app.
In fact, making the app offline-first and storing the data locally is the first step towards having a local-first app.
Effect and pglite
The app logic is organized in separated services defined using effect
.
pnpm add effect @electric-sql/pglite
@electric-sql/pglite
exports a PGlite
class used to initialize the database. We are going to wrap PGlite.create
using Effect.Service
:
We use IndexDb to store the data locally, passing the path to the database extracted from
Config
toPGlite.create
with theidb://
prefix.
import * as _PGlite from "@electric-sql/pglite";
import { Config, Data, Effect } from "effect";
class PgliteError extends Data.TaggedError("PgliteError")<{
cause: unknown;
}> {}
export class Pglite extends Effect.Service<Pglite>()("Pglite", {
effect: Effect.gen(function* () {
const indexDb = yield* Config.string("INDEX_DB");
const client = yield* Effect.tryPromise({
try: () => _PGlite.PGlite.create(`idb://${indexDb}`),
catch: (error) => new PgliteError({ cause: error }),
});
return { client };
}),
}) {}
Query function and drizzle
On top of exporting client
we also define a query
helper function that uses drizzle-orm
to query the database.
The first step is wrapping client
with drizzle
:
export class Pglite extends Effect.Service<Pglite>()("Pglite", {
effect: Effect.gen(function* () {
const indexDb = yield* Config.string("INDEX_DB");
const client = yield* Effect.tryPromise({
try: () => _PGlite.PGlite.create(`idb://${indexDb}`),
catch: (error) => new PgliteError({ cause: error }),
});
const orm = drizzle({ client });
return { client, orm };
}),
}) {}
Then we can use orm
to create a wrapper function to query the database:
export class Pglite extends Effect.Service<Pglite>()("Pglite", {
effect: Effect.gen(function* () {
const indexDb = yield* Config.string("INDEX_DB");
const client = yield* Effect.tryPromise({
try: () => _PGlite.PGlite.create(`idb://${indexDb}`),
catch: (error) => new PgliteError({ cause: error }),
});
const orm = drizzle({ client });
const query = <R>(execute: (_: typeof orm) => Promise<R>) =>
Effect.tryPromise({
try: () => execute(orm),
catch: (error) => new PgliteError({ cause: error }),
});
return { client, orm, query };
}),
}) {}
With this we have all we need to create and query the local database.