Pglite service with effect

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 to PGlite.create with the idb:// prefix.

/services/pglite.ts
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.