Client effect runtime

We have now defined all the services inside the app. The last step is to export a Runtime that combines all the services to execute the app logic.

If you want to learn more on how to structure an app with effect, services and Runtime, check out Effect: Beginners Complete Getting Started.

If you are interested specifically about React (19), see Effect with React 19: Project Template.

Since we used Effect.Service each service has a Default property containing a Layer with the service default instance.

We merge all the layers inside MainLayer:

/services/runtime-client.ts
const MainLayer = Layer.mergeAll(
  WriteApi.Default,
  ReadApi.Default,
  Migrations.Default,
  Pglite.Default
);

Config provider

Remember how we used Config to define INDEX_DB in the pglite service?

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 };
  }),
}) {}

When building the runtime we need to apply a valid Config to MainLayer. We do that using ConfigProvider.fromMap:

const CustomConfigProvider = Layer.setConfigProvider(
  ConfigProvider.fromMap(new Map([["INDEX_DB", "v1"]]))
);

We then need to provide CustomConfigProvider to MainLayer:

Make sure to not include CustomConfigProvider in the list of mergeAll but instead only later as a separate Layer.provide.

That's because CustomConfigProvider must be provided to each service in the list instead of alongside them.

const CustomConfigProvider = Layer.setConfigProvider(
  ConfigProvider.fromMap(new Map([["INDEX_DB", "v1"]]))
);

const MainLayer = Layer.mergeAll(
  WriteApi.Default,
  ReadApi.Default,
  Migrations.Default,
  Pglite.Default
).pipe(Layer.provide(CustomConfigProvider));

Finally, we can export a RuntimeClient using ManagedRuntime:

const CustomConfigProvider = Layer.setConfigProvider(
  ConfigProvider.fromMap(new Map([["INDEX_DB", "v1"]]))
);

const MainLayer = Layer.mergeAll(
  WriteApi.Default,
  ReadApi.Default,
  Migrations.Default,
  Pglite.Default
).pipe(Layer.provide(CustomConfigProvider));

export const RuntimeClient = ManagedRuntime.make(MainLayer);

When executing effects everywhere in the app we can import RuntimeClient (e.g. RuntimeClient.runPromise).