Skip to content

Instantly share code, notes, and snippets.

@rohovskoi
Last active January 12, 2026 02:28
Show Gist options
  • Select an option

  • Save rohovskoi/7bd905eb5e965011708898d6d78cfd6e to your computer and use it in GitHub Desktop.

Select an option

Save rohovskoi/7bd905eb5e965011708898d6d78cfd6e to your computer and use it in GitHub Desktop.
(POC): DrizzleEffectBridge translates drizzle-orm/effect-postgres's Effect-based execution model to Promise-based expectations from third-party libraries.
import * as Effect from 'effect/Effect';
import * as Runtime from 'effect/Runtime';
/** Represents any object that can be wrapped by the proxy */
type AnyObject = Record<string | symbol, unknown>;
/** The thenable interface that a third-party library expects when awaiting queries */
type ThenableFunction<T> = (
onfulfilled?: (value: T) => unknown,
onrejected?: (reason: unknown) => unknown,
) => Promise<unknown>;
/** An Effect-based Drizzle query builder with an execute method */
interface EffectQueryBuilder {
readonly [Effect.EffectTypeId]: unknown;
execute: () => Effect.Effect<unknown>;
}
/** Transaction callback signature expected by Better Auth */
type TransactionCallback<T> = (tx: T) => Promise<unknown>;
/**
* Type guard: Check if an object has the Effect protocol (for proxied objects).
*/
const hasEffectTypeId = (
obj: unknown,
): obj is { [Effect.EffectTypeId]: unknown } => {
return obj !== null && typeof obj === 'object' && Effect.EffectTypeId in obj;
};
/**
* Type guard: Check if an object is an Effect query builder.
* These are drizzle-orm/effect-postgres query builders (Select, Insert, Delete, Update)
* that have the Effect protocol mixed in via applyEffectWrapper.
*/
const isEffectQueryBuilder = (obj: unknown): obj is EffectQueryBuilder => {
return (
hasEffectTypeId(obj) &&
'execute' in obj &&
typeof obj.execute === 'function'
);
};
/**
* Type guard: Check if a value is an object (not null, not primitive).
*/
const isObject = (value: unknown): value is AnyObject => {
return value !== null && typeof value === 'object';
};
/**
* Type guard: Check if an object is a native Promise or thenable (but not an Effect).
*/
const isNativePromiseOrThenable = (obj: unknown): obj is Promise<unknown> => {
if (!isObject(obj)) return false;
if (obj instanceof Promise) return true;
return (
'then' in obj && typeof obj.then === 'function' && !hasEffectTypeId(obj)
);
}; /**
* DrizzleEffectBridge bridges Promise-based expectations
* with drizzle-orm/effect-postgres's Effect-based execution model.
*
* It wraps the Effect-based Drizzle database instance with a Proxy that:
* 1. Intercepts `.then` calls to execute Effects and return Promises (handled by Effect Runtime)
* 2. Recursively wraps query builders to maintain the bridge through method chaining
* 3. Handles transactions by wrapping the transaction context
*/
export class DrizzleEffectBridge extends Effect.Service<DrizzleEffectBridge>()(
'DrizzleEffectBridge',
{
effect: Effect.gen(function* () {
const runtime = yield* Effect.runtime<never>();
const runPromise = Runtime.runPromise(runtime);
const cache = new WeakMap<AnyObject, AnyObject>();
/**
* Wraps a Drizzle Effect-based database/query builder to make it
* compatible with Better Auth's Promise-based expectations.
*
* Uses a generic type parameter to preserve the original type through
* the proxy wrapper, ensuring Better Auth retains full type inference.
*/
const customEffectToPromiseWrapper = <T>(obj: T): T => {
if (!isObject(obj)) {
return obj;
}
if (isNativePromiseOrThenable(obj)) {
return obj;
}
const cached = cache.get(obj);
if (cached !== undefined) {
return cached as T;
}
const proxy = new Proxy(obj, {
get(
target: AnyObject,
prop: string | symbol,
receiver: unknown,
): unknown {
// Preserve Symbol properties for Effect and Drizzle internals
if (typeof prop === 'symbol') {
return Reflect.get(target, prop, target);
}
// Preserve Drizzle's internal state accessor
if (prop === '_') {
return Reflect.get(target, prop, target);
}
// Intercept `.then` to bridge Effect → Promise
// When Better Auth awaits a query, this converts the Effect execution to a Promise
if (prop === 'then') {
return createThenHandler(target);
}
const value = Reflect.get(target, prop, receiver);
// Wrap functions to ensure their return values are also wrapped
if (typeof value === 'function') {
if (prop === 'transaction') {
return createTransactionHandler(target);
}
return createMethodWrapper(target, value);
}
// Recursively wrap nested objects (e.g., db.query)
if (isObject(value)) {
return customEffectToPromiseWrapper(value);
}
return value;
},
});
cache.set(obj, proxy);
return proxy as T;
};
/**
* Creates a `.then` handler that executes the Effect and returns a Promise.
* Returns undefined for non-Effect objects to prevent false thenable detection.
*/
const createThenHandler = (
target: AnyObject,
): ThenableFunction<unknown> | undefined => {
// Effect query builders: call execute() to get the Effect, then run it
if (isEffectQueryBuilder(target)) {
return (onfulfilled, onrejected) => {
const effect = target.execute();
return runPromise(effect).then(onfulfilled, onrejected);
};
}
// Raw Effects (e.g., from transaction results): run directly
if (hasEffectTypeId(target)) {
return (onfulfilled, onrejected) => {
return runPromise(target as Effect.Effect<unknown>).then(
onfulfilled,
onrejected,
);
};
}
// Not an Effect - return undefined so it's not treated as a thenable
return undefined;
};
/**
* Creates a transaction handler that wraps the transaction context
* and bridges the Effect-based transaction with Promise-based callbacks.
*/
const createTransactionHandler = (target: AnyObject) => {
return (
callback: TransactionCallback<AnyObject>,
config?: unknown,
): Promise<unknown> => {
const txMethod = target.transaction as (
cb: (tx: unknown) => Effect.Effect<unknown>,
config?: unknown,
) => Effect.Effect<unknown>;
const effect = txMethod(
(tx: unknown) =>
Effect.promise(() =>
callback(customEffectToPromiseWrapper(tx as AnyObject)),
),
config,
);
return runPromise(effect);
};
};
/**
* Creates a wrapper for methods that ensures return values are also wrapped.
* This maintains the Effect to Promise bridge through method chaining.
*/
const createMethodWrapper = (target: AnyObject, method: Function) => {
return (...args: Array<unknown>): unknown => {
const result = method.apply(target, args);
if (isObject(result)) {
return customEffectToPromiseWrapper(result);
}
return result;
};
};
return {
customEffectToPromiseWrapper,
} as const;
}),
},
) {}
export const DrizzleEffectBridgeLive = DrizzleEffectBridge.Default;
// ------------------------------------------------------------------
// USAGE:
// Inject your DrizzleEffectBridge into `Effect.gen()`,
// and don't forget to add its DrizzleEffectBridgeLive layer to the list of `dependencies: [DrizzleEffectBridgeLive]`.
// If you don't provide the DrizzleEffectBridgeLive to the layer your run, you will get an unsatisfied requirement error.
const { customEffectToPromiseWrapper } = yield* DrizzleEffectBridge;
const translatedDb = customEffectToPromiseWrapper(db);
const auth = betterAuth({
baseURL: APP.API_URL,
basePath: '/auth',
database: drizzleAdapter(translatedDb, {
provider: 'pg',
schema: {
user,
account,
verification,
invitation,
session
},
transaction: false,
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment