Replace mutable singletons and getter functions with dependency injection services for configuration-dependent components. This approach eliminates race conditions, provides proper initialization guarantees, and enables environment-specific configuration management.
Instead of using mutable globals or getter functions:
let client: ReturnType<typeof createClient<Database>> | null = null
export const getSupabaseClient = () =>
Effect.gen(function*() {
if (client) {
return client
}
// initialization logic...
client = createClient<Database>(url, anonKey)
return client
})
Use Effect services with proper environment variable validation:
export class SupabaseClient extends Effect.Service<SupabaseClient>()("SupabaseClient", {
scoped: (options?: SupabaseOptions | undefined) =>
Effect.gen(function*() {
const url = yield* S.decode(S.URL)(PUBLIC_SUPABASE_URL).pipe(
Effect.mapError((cause) =>
new SupabaseClientError({
operation: "init",
message: "Could not decode PUBLIC_SUPABASE_URL to URL",
cause,
})
),
)
const anonKey = yield* S.decode(S.NonEmptyString)(PUBLIC_SUPABASE_ANON_KEY).pipe(
Effect.mapError((cause) =>
new SupabaseClientError({
operation: "init",
message: "Could not decode PUBLIC_SUPABASE_ANON_KEY to non-empty string",
cause,
})
),
)
return createClient<Database>(url.toString(), anonKey, options)
}),
}) {}
This pattern ensures configuration is validated at startup, supports environment-specific layers (test vs production), and provides type-safe dependency injection throughout the application.
Enter the URL of a public GitHub repository