Circular reference error in better-call dependency breaks auth endpoints #1647

Closed
opened 2026-03-13 08:53:23 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @RikhiSingh on GitHub (Aug 5, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Set up better-auth with Drizzle ORM and database relations
   // users table with relation to contacts
   usersTable: pgTable('users', {
     id: serial('id').primaryKey(),
     // ... other fields
   }, (table) => ({
     contact: one('contacts', {
       fields: [table.id],
       references: [contactsTable.userId],
     }),
   }))

   // contacts table with relation back to users
   contactsTable: pgTable('contacts', {
     userId: integer('user_id').references(() => usersTable.id),
     // ... other fields
   }, (table) => ({
     user: one('users', {
       fields: [table.userId],
       references: [usersTable.id],
     }),
   }))
  1. Attempt to sign up a user via /auth/sign-up/email endpoint
  2. Observe the error in the API logs

Current vs. Expected behavior

Current Behavior:

TypeError: JSON.stringify cannot serialize cyclic structures.
at toResponse (node_modules/.pnpm/better-call@1.0.12/node_modules/better-call/dist/index.js:195:17)

Expected Behavior:

  • Auth endpoints should work normally with database relations
  • Circular references should be handled gracefully or serialized safely
  • No JSON serialization errors should occu

What version of Better Auth are you using?

better-auth@1.3.4 better-call@1.0.12 (dependency)

Provide environment information

Operating System:
- macOS
- Windows 11

Node.js Version:
- Node.js 18+ or Bun 1.0+

Database:
- PostgreSQL with Drizzle ORM

Package Manager:
- pnpm

Which area(s) are affected? (Select all that apply)

Client, Backend

Auth config (if applicable)

import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/drizzle';
import { username, anonymous, twoFactor, emailOTP, magicLink, oneTap, customSession } from 'better-auth/plugins';

export const auth = betterAuth({
  appName: 'My App',
  secret: process.env.AUTH_SECRET!,
  database: drizzleAdapter(db, {
    provider: 'pg',
    schema: {
      user: schema.usersTable,
      session: schema.sessionsTable,
      account: schema.accountTable,
      verification: schema.verificationTable,
      passkey: schema.passkeyTable,
      twoFactor: schema.twoFactorTable,
    },
  }),
  plugins: [
    username({
      minUsernameLength: 3,
      maxUsernameLength: 20,
      schema: {
        user: {
          fields: {
            username: 'username',
          },
        },
      },
    }),
    anonymous(),
    twoFactor({
      issuer: 'My App',
      otpOptions: {
        async sendOTP({ user, otp }) {
          // Email sending logic
        },
        digits: 6,
      },
    }),
    emailOTP({
      async sendVerificationOTP({ email, otp, type }) {
        // Email sending logic
      },
      otpLength: 6,
    }),
    magicLink({
      sendMagicLink: async ({ email, token, url }) => {
        // Email sending logic
      },
    }),
    oneTap(),
    customSession(async ({ user, session }) => {
      // This is where the circular reference occurs
      const contact = await db.query.contactsTable.findFirst({
        where: (contacts, { eq }) => eq(contacts.userId, Number(user.id)),
        columns: {
          id: true,
          status: true,
        },
      });

      return {
        user,
        session,
        contact: contact || undefined,
      };
    }),
  ],
});

Additional context

The issue stems from the better-call dependency (v1.0.12) used internally by better-auth. The toResponse function in better-call attempts to serialize response data using JSON.stringify without circular reference detection. When Drizzle ORM returns objects with relations, circular references are created between related tables (e.g., users ↔ contacts).

I have created issue and PR on better-call itself:

Issue: https://github.com/Bekacru/better-call/issues/27
PR: https://github.com/Bekacru/better-call/pull/28

Impact:

  • All auth endpoints that return user data with relations are affected
  • Sign-up, sign-in, and session endpoints fail
  • Affects any better-auth implementation with database relations
Originally created by @RikhiSingh on GitHub (Aug 5, 2025). ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce 1. Set up better-auth with Drizzle ORM and database relations ```tsx // users table with relation to contacts usersTable: pgTable('users', { id: serial('id').primaryKey(), // ... other fields }, (table) => ({ contact: one('contacts', { fields: [table.id], references: [contactsTable.userId], }), })) // contacts table with relation back to users contactsTable: pgTable('contacts', { userId: integer('user_id').references(() => usersTable.id), // ... other fields }, (table) => ({ user: one('users', { fields: [table.userId], references: [usersTable.id], }), })) ``` 2. Attempt to sign up a user via /auth/sign-up/email endpoint 3. Observe the error in the API logs ### Current vs. Expected behavior Current Behavior: ```bash TypeError: JSON.stringify cannot serialize cyclic structures. at toResponse (node_modules/.pnpm/better-call@1.0.12/node_modules/better-call/dist/index.js:195:17) ``` Expected Behavior: - Auth endpoints should work normally with database relations - Circular references should be handled gracefully or serialized safely - No JSON serialization errors should occu ### What version of Better Auth are you using? better-auth@1.3.4 better-call@1.0.12 (dependency) ### Provide environment information ```bash Operating System: - macOS - Windows 11 Node.js Version: - Node.js 18+ or Bun 1.0+ Database: - PostgreSQL with Drizzle ORM Package Manager: - pnpm ``` ### Which area(s) are affected? (Select all that apply) Client, Backend ### Auth config (if applicable) ```typescript import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/drizzle'; import { username, anonymous, twoFactor, emailOTP, magicLink, oneTap, customSession } from 'better-auth/plugins'; export const auth = betterAuth({ appName: 'My App', secret: process.env.AUTH_SECRET!, database: drizzleAdapter(db, { provider: 'pg', schema: { user: schema.usersTable, session: schema.sessionsTable, account: schema.accountTable, verification: schema.verificationTable, passkey: schema.passkeyTable, twoFactor: schema.twoFactorTable, }, }), plugins: [ username({ minUsernameLength: 3, maxUsernameLength: 20, schema: { user: { fields: { username: 'username', }, }, }, }), anonymous(), twoFactor({ issuer: 'My App', otpOptions: { async sendOTP({ user, otp }) { // Email sending logic }, digits: 6, }, }), emailOTP({ async sendVerificationOTP({ email, otp, type }) { // Email sending logic }, otpLength: 6, }), magicLink({ sendMagicLink: async ({ email, token, url }) => { // Email sending logic }, }), oneTap(), customSession(async ({ user, session }) => { // This is where the circular reference occurs const contact = await db.query.contactsTable.findFirst({ where: (contacts, { eq }) => eq(contacts.userId, Number(user.id)), columns: { id: true, status: true, }, }); return { user, session, contact: contact || undefined, }; }), ], }); ``` ### Additional context The issue stems from the better-call dependency (v1.0.12) used internally by better-auth. The toResponse function in better-call attempts to serialize response data using JSON.stringify without circular reference detection. When Drizzle ORM returns objects with relations, circular references are created between related tables (e.g., users ↔ contacts). I have created issue and PR on better-call itself: Issue: https://github.com/Bekacru/better-call/issues/27 PR: https://github.com/Bekacru/better-call/pull/28 Impact: - All auth endpoints that return user data with relations are affected - Sign-up, sign-in, and session endpoints fail - Affects any better-auth implementation with database relations
GiteaMirror added the bug label 2026-03-13 08:53:23 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 5, 2025):

Hi @RikhiSingh! I'm Dosu and I’m helping the better-auth team.

This is a known integration issue: better-auth relies on better-call for response serialization, and better-call currently does not handle circular references in returned objects. When Drizzle ORM returns objects with bi-directional relations (like users ↔ contacts), this creates circular references that cause JSON.stringify to throw an error during endpoint responses.

There are no built-in mechanisms in better-auth to detect or sanitize circular references before serialization—responsibility for safe serialization is left to the user or to better-call itself. Until better-call merges a fix, you’ll need to ensure that any objects returned from customSession or similar plugins are plain and do not contain circular references (for example, by selecting only the fields you need, or by flattening/narrowing the returned objects) see code reference.

There’s no documentation or best practice in better-auth for this yet, but this pattern is necessary for any implementation using Drizzle ORM relations until the upstream better-call issue is resolved.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Aug 5, 2025): <!-- Greeting --> Hi @RikhiSingh! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known integration issue: better-auth relies on better-call for response serialization, and better-call currently does not handle circular references in returned objects. When Drizzle ORM returns objects with bi-directional relations (like users ↔ contacts), this creates circular references that cause JSON.stringify to throw an error during endpoint responses. There are no built-in mechanisms in better-auth to detect or sanitize circular references before serialization—responsibility for safe serialization is left to the user or to better-call itself. Until better-call merges a fix, you’ll need to ensure that any objects returned from customSession or similar plugins are plain and do not contain circular references (for example, by selecting only the fields you need, or by flattening/narrowing the returned objects) [see code reference](https://github.com/better-auth/better-auth/blob/669ce3e6b737ce7c1a9ce982bf092f00d7373fbc/packages/better-auth/src/plugins/custom-session/index.ts). There’s no documentation or best practice in better-auth for this yet, but this pattern is necessary for any implementation using Drizzle ORM relations until the upstream better-call issue is resolved. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/208a5da6-b87d-4a64-a209-630e342a9f7f?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3813)
Author
Owner

@himself65 commented on GitHub (Aug 6, 2025):

Related PR: https://github.com/Bekacru/better-call/pull/29 and https://github.com/Bekacru/better-call/pull/28

@himself65 commented on GitHub (Aug 6, 2025): Related PR: https://github.com/Bekacru/better-call/pull/29 and https://github.com/Bekacru/better-call/pull/28
Author
Owner

@Kinfe123 commented on GitHub (Aug 6, 2025):

already merged. closing this.

@Kinfe123 commented on GitHub (Aug 6, 2025): already merged. closing this.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1647