Summary
@better-auth/agent-auth currently creates an in-memory JTI cache during agentAuth() plugin construction. That cache starts a setInterval() in its constructor, which breaks Convex deployments because Convex does not allow timers during module import / auth route initialization.
This means simply adding agentAuth(...) to a Better Auth + Convex integration can make normal auth endpoints fail before any Agent Auth route is used.
Environment
@better-auth/agent-auth: 0.6.2
better-auth: 1.6.15
@convex-dev/better-auth: 0.12.4
- Runtime: Convex functions / Convex HTTP auth routes
Reproduction
Register Agent Auth in a Convex-backed Better Auth config:
import { agentAuth } from "@better-auth/agent-auth";
import { betterAuth } from "better-auth/minimal";
import { convex } from "@convex-dev/better-auth/plugins";
export const auth = betterAuth({
plugins: [
agentAuth({
modes: ["delegated"],
approvalMethods: ["device_authorization"],
allowDynamicHostRegistration: false,
capabilities: [
{
name: "example_capability",
title: "Example capability",
description: "Example capability",
approvalStrength: "session",
},
],
}),
convex({
authConfig,
jwt: { expirationSeconds: 15 * 60 },
}),
],
});
Then start/deploy Convex and hit a normal auth route, for example an email sign-in endpoint.
Actual behavior
The auth endpoint fails with a server error before the request can complete.
Observed error:
Uncaught Error: setInterval unsupported at import time
The relevant stack path is:
MemoryJtiCache
JtiCacheProxy
agentAuth
createAuthOptions
registerRoutes
Root cause
In the published 0.6.2 build, agentAuth() constructs a JtiCacheProxy before plugin init(ctx) runs:
const jtiCache = new JtiCacheProxy();
JtiCacheProxy immediately creates a MemoryJtiCache:
constructor() {
this.inner = new MemoryJtiCache();
}
MemoryJtiCache starts a timer immediately:
constructor() {
this.cleanupInterval = setInterval(() => this.evict(), 30000);
}
This happens during plugin construction / module initialization, which Convex rejects.
The jtiCacheStorage: "secondary-storage" option does not avoid the crash, because switching to secondary storage happens later inside init(ctx). The memory cache has already been constructed by then.
Expected behavior
agentAuth() should not start timers or create runtime-only resources during plugin construction/import.
For Convex and similar runtimes, it should be possible to configure Agent Auth without triggering setInterval() before init(ctx).
Suggested fix
Make the JTI cache lazy or initialize it inside init(ctx) after storage selection is known.
For example:
JtiCacheProxy starts with no inner cache.
init(ctx) chooses SecondaryStorageJtiCache when jtiCacheStorage === "secondary-storage" and ctx.secondaryStorage is available.
MemoryJtiCache is only created lazily if memory storage is actually selected.
- No
setInterval() is called during agentAuth() plugin construction.
A sketch:
class JtiCacheProxy {
private inner: JtiCache | null = null;
useSecondaryStorage(storage: SecondaryStorage) {
this.inner?.destroy?.();
this.inner = new SecondaryStorageJtiCache(storage);
}
private memory() {
this.inner ??= new MemoryJtiCache();
return this.inner;
}
has(jti: string) {
return (this.inner ?? this.memory()).has(jti);
}
add(jti: string, maxAgeSec: number) {
return (this.inner ?? this.memory()).add(jti, maxAgeSec);
}
}
There is also a WebAuthnChallengeCache.ensureSweep() timer. It is not the immediate import-time failure described here, but runtimes that disallow timers generally may need a similar no-timer/lazy cleanup path there too.
Workaround
The only safe workaround on Convex right now is to feature-flag agentAuth(...) off entirely:
const enableAgentAuth = process.env.ENABLE_AGENT_AUTH === "1";
plugins: [
// other plugins...
...(enableAgentAuth ? [agentAuth(options)] : []),
convex(...),
]
That restores normal auth endpoints, but disables Agent Auth.
Summary
@better-auth/agent-authcurrently creates an in-memory JTI cache duringagentAuth()plugin construction. That cache starts asetInterval()in its constructor, which breaks Convex deployments because Convex does not allow timers during module import / auth route initialization.This means simply adding
agentAuth(...)to a Better Auth + Convex integration can make normal auth endpoints fail before any Agent Auth route is used.Environment
@better-auth/agent-auth:0.6.2better-auth:1.6.15@convex-dev/better-auth:0.12.4Reproduction
Register Agent Auth in a Convex-backed Better Auth config:
Then start/deploy Convex and hit a normal auth route, for example an email sign-in endpoint.
Actual behavior
The auth endpoint fails with a server error before the request can complete.
Observed error:
The relevant stack path is:
Root cause
In the published
0.6.2build,agentAuth()constructs aJtiCacheProxybefore plugininit(ctx)runs:JtiCacheProxyimmediately creates aMemoryJtiCache:MemoryJtiCachestarts a timer immediately:This happens during plugin construction / module initialization, which Convex rejects.
The
jtiCacheStorage: "secondary-storage"option does not avoid the crash, because switching to secondary storage happens later insideinit(ctx). The memory cache has already been constructed by then.Expected behavior
agentAuth()should not start timers or create runtime-only resources during plugin construction/import.For Convex and similar runtimes, it should be possible to configure Agent Auth without triggering
setInterval()beforeinit(ctx).Suggested fix
Make the JTI cache lazy or initialize it inside
init(ctx)after storage selection is known.For example:
JtiCacheProxystarts with noinnercache.init(ctx)choosesSecondaryStorageJtiCachewhenjtiCacheStorage === "secondary-storage"andctx.secondaryStorageis available.MemoryJtiCacheis only created lazily if memory storage is actually selected.setInterval()is called duringagentAuth()plugin construction.A sketch:
There is also a
WebAuthnChallengeCache.ensureSweep()timer. It is not the immediate import-time failure described here, but runtimes that disallow timers generally may need a similar no-timer/lazy cleanup path there too.Workaround
The only safe workaround on Convex right now is to feature-flag
agentAuth(...)off entirely:That restores normal auth endpoints, but disables Agent Auth.