1import type { RuntimeContext } from "alchemy";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import type { BetterAuthStorageError } from "./Errors.ts";
5
6/**
7 * Effect-native contract for Better Auth's `secondaryStorage` — the
8 * key-value store used for sessions, rate limiting, and OAuth state when
9 * configured.
10 *
11 * All five operations are required. TTLs are in SECONDS.
12 */
13export interface SecondaryStorageService {
14 readonly get: (
15 key: string,
16 ) => Effect.Effect<string | null, BetterAuthStorageError, RuntimeContext>;
17 readonly set: (
18 key: string,
19 value: string,
20 ttlSeconds?: number,
21 ) => Effect.Effect<void, BetterAuthStorageError, RuntimeContext>;
22 readonly delete: (
23 key: string,
24 ) => Effect.Effect<void, BetterAuthStorageError, RuntimeContext>;
25 /** Atomic read-and-delete. */
26 readonly getAndDelete: (
27 key: string,
28 ) => Effect.Effect<string | null, BetterAuthStorageError, RuntimeContext>;
29 /**
30 * Atomically increment a counter. When the key is absent it is created
31 * at `1` with `ttlSeconds`; later increments never extend the TTL
32 * (fixed window).
33 */
34 readonly increment: (
35 key: string,
36 ttlSeconds: number,
37 ) => Effect.Effect<number, BetterAuthStorageError, RuntimeContext>;
38}
39
40/**
41 * Optional secondary-storage dependency of {@link BetterAuth}.
42 *
43 * When a layer providing this service is in context, Better Auth is
44 * configured with `secondaryStorage` and its rate limiter defaults to it.
45 * When absent, Better Auth runs database-only — the dependency is resolved
46 * with `Effect.serviceOption`, so it never appears in `BetterAuth`'s
47 * requirements.
48 *
49 * Better Auth's guidance is a Redis-style store: strongly consistent
50 * reads and precise sub-minute TTLs. Eventually-consistent stores
51 * (e.g. Cloudflare KV) are NOT a fit — session reads race cross-location
52 * propagation and rate-limit windows need atomic counters. A strongly
53 * consistent Cloudflare option would be a Durable Object-backed layer.
54 */
55export class SecondaryStorage extends Context.Service<
56 SecondaryStorage,
57 SecondaryStorageService
58>()("BetterAuth.SecondaryStorage") {}
59
60/**
61 * Bridge the Effect-native service to the promise interface Better Auth
62 * expects, running each operation in the captured runtime context.
63 *
64 * @internal
65 */
66export const toPromiseStorage = (
67 storage: SecondaryStorageService,
68 runPromise: <A, E>(effect: Effect.Effect<A, E, RuntimeContext>) => Promise<A>,
69) => ({
70 get: (key: string) => runPromise(storage.get(key)),
71 set: (key: string, value: string, ttl?: number) =>
72 runPromise(storage.set(key, value, ttl)),
73 delete: (key: string) => runPromise(storage.delete(key)),
74 getAndDelete: (key: string) => runPromise(storage.getAndDelete(key)),
75 increment: (key: string, ttl: number) =>
76 runPromise(storage.increment(key, ttl)),
77});