Skip to content

agentAuth fails in Convex because MemoryJtiCache calls setInterval during plugin construction #34

Description

@Deniffer001

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions