MagicLink api unresolved #2335

Closed
opened 2026-03-13 09:44:56 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @whitetown on GitHub (Nov 19, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I added magicLink plugin as described on the website.
it works on the client side but when I wanted to call it server side it says "INVALID URL"

const data = await auth.api.signInMagicLink({
        body: {
            email: email,
            name: name,
            // callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/onboarding`,
            // newUserCallbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/onboarding`,
            // errorCallbackURL: `${process.env.NEXT_PUBLIC_APP_URL}`,
        },
        // This endpoint requires session cookies.
        headers: await headers(),
    })

Current vs. Expected behavior

for some reason auth.api is undefined

Image

What version of Better Auth are you using?

1.3.27

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:51 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8112",
    "release": "24.6.0",
    "cpuCount": 8,
    "cpuModel": "Apple M2",
    "totalMemory": "24.00 GB",
    "freeMemory": "0.15 GB"
  },
  "node": {
    "version": "v22.16.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.5.5"
    },
    {
      "name": "react",
      "version": "^19.2.0"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "drizzle",
      "version": "^0.44.6"
    }
  ],
  "betterAuth": {
    "version": "1.3.27",
    "config": {
      "telemetry": {
        "enabled": false
      },
      "databaseHooks": {
        "user": {
          "create": {}
        },
        "session": {
          "create": {}
        }
      },
      "emailAndPassword": {
        "enabled": true,
        "autoSignIn": true,
        "requireEmailVerification": false
      },
      "emailVerification": {
        "sendOnSignUp": true,
        "autoSignInAfterVerification": true
      },
      "user": {
        "changeEmail": {
          "enabled": true
        }
      },
      "socialProviders": {
        "github": {},
        "google": {}
      },
      "account": {
        "accountLinking": {
          "enabled": true,
          "allowDifferentEmails": true
        }
      },
      "plugins": [
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "adminRoles": [
                "admin",
                "superadmin"
              ],
              "adminUserIds": [
                "WFqIFTNfqmGkKT3Q2hwFVDGKSarxxoWN",
                "D5E25QkodZeo9DVkPzaquvIbuUDq1uxN"
              ]
            }
          }
        },
        {
          "name": "jwt",
          "config": {
            "id": "jwt",
            "options": {},
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "jwks": {
                "fields": {
                  "publicKey": {
                    "type": "string",
                    "required": true
                  },
                  "privateKey": {
                    "type": "string",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  }
                }
              }
            }
          }
        },
        {
          "name": "api-key",
          "config": {
            "id": "api-key",
            "hooks": {
              "before": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "apikey": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "start": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "prefix": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "key": {
                    "type": "string",
                    "required": true,
                    "input": false
                  },
                  "userId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    },
                    "required": true,
                    "input": false
                  },
                  "refillInterval": {
                    "type": "number",
                    "required": false,
                    "input": false
                  },
                  "refillAmount": {
                    "type": "number",
                    "required": false,
                    "input": false
                  },
                  "lastRefillAt": {
                    "type": "date",
                    "required": false,
                    "input": false
                  },
                  "enabled": {
                    "type": "boolean",
                    "required": false,
                    "input": false,
                    "defaultValue": true
                  },
                  "rateLimitEnabled": {
                    "type": "boolean",
                    "required": false,
                    "input": false,
                    "defaultValue": true
                  },
                  "rateLimitTimeWindow": {
                    "type": "number",
                    "required": false,
                    "input": false,
                    "defaultValue": 86400000
                  },
                  "rateLimitMax": {
                    "type": "number",
                    "required": false,
                    "input": false,
                    "defaultValue": 10
                  },
                  "requestCount": {
                    "type": "number",
                    "required": false,
                    "input": false,
                    "defaultValue": 0
                  },
                  "remaining": {
                    "type": "number",
                    "required": false,
                    "input": false
                  },
                  "lastRequest": {
                    "type": "date",
                    "required": false,
                    "input": false
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": false,
                    "input": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true,
                    "input": false
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": true,
                    "input": false
                  },
                  "permissions": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "metadata": {
                    "type": "string",
                    "required": false,
                    "input": true,
                    "transform": {}
                  }
                }
              }
            }
          }
        },
        {
          "name": "phone-number",
          "config": {
            "id": "phone-number",
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "phoneNumber": {
                    "type": "string",
                    "required": false,
                    "unique": true,
                    "sortable": true,
                    "returned": true
                  },
                  "phoneNumberVerified": {
                    "type": "boolean",
                    "required": false,
                    "returned": true,
                    "input": false
                  }
                }
              }
            },
            "rateLimit": [
              {
                "window": 60000,
                "max": 10
              }
            ],
            "$ERROR_CODES": {
              "INVALID_PHONE_NUMBER": "Invalid phone number",
              "PHONE_NUMBER_EXIST": "Phone number already exists",
              "INVALID_PHONE_NUMBER_OR_PASSWORD": "[REDACTED]",
              "UNEXPECTED_ERROR": "Unexpected error",
              "OTP_NOT_FOUND": "OTP not found",
              "OTP_EXPIRED": "OTP expired",
              "INVALID_OTP": "Invalid OTP",
              "PHONE_NUMBER_NOT_VERIFIED": "Phone number not verified"
            }
          }
        },
        {
          "name": "passkey",
          "config": {
            "id": "passkey",
            "endpoints": {},
            "schema": {
              "passkey": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": false
                  },
                  "publicKey": {
                    "type": "string",
                    "required": true
                  },
                  "userId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "required": true
                  },
                  "credentialID": {
                    "type": "string",
                    "required": true
                  },
                  "counter": {
                    "type": "number",
                    "required": true
                  },
                  "deviceType": {
                    "type": "string",
                    "required": true
                  },
                  "backedUp": {
                    "type": "boolean",
                    "required": true
                  },
                  "transports": {
                    "type": "string",
                    "required": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": false
                  },
                  "aaguid": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            
          }
        },
        {
          "name": "magic-link",
          "config": {
            "id": "magic-link",
            "endpoints": {},
            "rateLimit": [
              {
                "window": 60,
                "max": 5
              }
            ]
          }
        },
        {
          "name": "organization",
          "config": {
            "id": "organization",
            "endpoints": {},
            "schema": {
              "organization": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": true,
                    "sortable": true
                  },
                  "slug": {
                    "type": "string",
                    "required": true,
                    "unique": true,
                    "sortable": true
                  },
                  "logo": {
                    "type": "string",
                    "required": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  }
                }
              },
              "member": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "role": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "member"
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  }
                }
              },
              "invitation": {
                "fields": {
                  "organizationId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "organization",
                      "field": "id"
                    }
                  },
                  "email": {
                    "type": "string",
                    "required": true,
                    "sortable": true
                  },
                  "role": {
                    "type": "string",
                    "required": false,
                    "sortable": true
                  },
                  "status": {
                    "type": "string",
                    "required": true,
                    "sortable": true,
                    "defaultValue": "pending"
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": true
                  },
                  "inviterId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "required": true
                  }
                }
              },
              "session": {
                "fields": {
                  "activeOrganizationId": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "$Infer": {
              "Organization": {},
              "Invitation": {},
              "Member": {},
              "Team": {},
              "TeamMember": {},
              "ActiveOrganization": {}
            }
            "options": {
              "membershipLimit": 10,
              "organizationCreation": {
                "disabled": false
              }
            }
          }
        },
        {
          "name": "custom-session",
          "config": {
            "id": "custom-session",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {}
          }
        }
      ],
      "hooks": {}
    }
  }
}

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @whitetown on GitHub (Nov 19, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I added magicLink plugin as described on the website. it works on the client side but when I wanted to call it server side it says "INVALID URL" ``` const data = await auth.api.signInMagicLink({ body: { email: email, name: name, // callbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/onboarding`, // newUserCallbackURL: `${process.env.NEXT_PUBLIC_APP_URL}/onboarding`, // errorCallbackURL: `${process.env.NEXT_PUBLIC_APP_URL}`, }, // This endpoint requires session cookies. headers: await headers(), }) ``` ### Current vs. Expected behavior for some reason auth.api is undefined <img width="522" height="227" alt="Image" src="https://github.com/user-attachments/assets/12669d27-d489-477d-ac5c-a1dfe150ab07" /> ### What version of Better Auth are you using? 1.3.27 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:51 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8112", "release": "24.6.0", "cpuCount": 8, "cpuModel": "Apple M2", "totalMemory": "24.00 GB", "freeMemory": "0.15 GB" }, "node": { "version": "v22.16.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "next", "version": "15.5.5" }, { "name": "react", "version": "^19.2.0" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "drizzle", "version": "^0.44.6" } ], "betterAuth": { "version": "1.3.27", "config": { "telemetry": { "enabled": false }, "databaseHooks": { "user": { "create": {} }, "session": { "create": {} } }, "emailAndPassword": { "enabled": true, "autoSignIn": true, "requireEmailVerification": false }, "emailVerification": { "sendOnSignUp": true, "autoSignInAfterVerification": true }, "user": { "changeEmail": { "enabled": true } }, "socialProviders": { "github": {}, "google": {} }, "account": { "accountLinking": { "enabled": true, "allowDifferentEmails": true } }, "plugins": [ { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "adminRoles": [ "admin", "superadmin" ], "adminUserIds": [ "WFqIFTNfqmGkKT3Q2hwFVDGKSarxxoWN", "D5E25QkodZeo9DVkPzaquvIbuUDq1uxN" ] } } }, { "name": "jwt", "config": { "id": "jwt", "options": {}, "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "jwks": { "fields": { "publicKey": { "type": "string", "required": true }, "privateKey": { "type": "string", "required": true }, "createdAt": { "type": "date", "required": true } } } } } }, { "name": "api-key", "config": { "id": "api-key", "hooks": { "before": [ {} ] }, "endpoints": {}, "schema": { "apikey": { "fields": { "name": { "type": "string", "required": false, "input": false }, "start": { "type": "string", "required": false, "input": false }, "prefix": { "type": "string", "required": false, "input": false }, "key": { "type": "string", "required": true, "input": false }, "userId": { "type": "string", "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "required": true, "input": false }, "refillInterval": { "type": "number", "required": false, "input": false }, "refillAmount": { "type": "number", "required": false, "input": false }, "lastRefillAt": { "type": "date", "required": false, "input": false }, "enabled": { "type": "boolean", "required": false, "input": false, "defaultValue": true }, "rateLimitEnabled": { "type": "boolean", "required": false, "input": false, "defaultValue": true }, "rateLimitTimeWindow": { "type": "number", "required": false, "input": false, "defaultValue": 86400000 }, "rateLimitMax": { "type": "number", "required": false, "input": false, "defaultValue": 10 }, "requestCount": { "type": "number", "required": false, "input": false, "defaultValue": 0 }, "remaining": { "type": "number", "required": false, "input": false }, "lastRequest": { "type": "date", "required": false, "input": false }, "expiresAt": { "type": "date", "required": false, "input": false }, "createdAt": { "type": "date", "required": true, "input": false }, "updatedAt": { "type": "date", "required": true, "input": false }, "permissions": { "type": "string", "required": false, "input": false }, "metadata": { "type": "string", "required": false, "input": true, "transform": {} } } } } } }, { "name": "phone-number", "config": { "id": "phone-number", "endpoints": {}, "schema": { "user": { "fields": { "phoneNumber": { "type": "string", "required": false, "unique": true, "sortable": true, "returned": true }, "phoneNumberVerified": { "type": "boolean", "required": false, "returned": true, "input": false } } } }, "rateLimit": [ { "window": 60000, "max": 10 } ], "$ERROR_CODES": { "INVALID_PHONE_NUMBER": "Invalid phone number", "PHONE_NUMBER_EXIST": "Phone number already exists", "INVALID_PHONE_NUMBER_OR_PASSWORD": "[REDACTED]", "UNEXPECTED_ERROR": "Unexpected error", "OTP_NOT_FOUND": "OTP not found", "OTP_EXPIRED": "OTP expired", "INVALID_OTP": "Invalid OTP", "PHONE_NUMBER_NOT_VERIFIED": "Phone number not verified" } } }, { "name": "passkey", "config": { "id": "passkey", "endpoints": {}, "schema": { "passkey": { "fields": { "name": { "type": "string", "required": false }, "publicKey": { "type": "string", "required": true }, "userId": { "type": "string", "references": { "model": "user", "field": "id" }, "required": true }, "credentialID": { "type": "string", "required": true }, "counter": { "type": "number", "required": true }, "deviceType": { "type": "string", "required": true }, "backedUp": { "type": "boolean", "required": true }, "transports": { "type": "string", "required": false }, "createdAt": { "type": "date", "required": false }, "aaguid": { "type": "string", "required": false } } } }, } }, { "name": "magic-link", "config": { "id": "magic-link", "endpoints": {}, "rateLimit": [ { "window": 60, "max": 5 } ] } }, { "name": "organization", "config": { "id": "organization", "endpoints": {}, "schema": { "organization": { "fields": { "name": { "type": "string", "required": true, "sortable": true }, "slug": { "type": "string", "required": true, "unique": true, "sortable": true }, "logo": { "type": "string", "required": false }, "createdAt": { "type": "date", "required": true }, "metadata": { "type": "string", "required": false } } }, "member": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" } }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" } }, "role": { "type": "string", "required": true, "sortable": true, "defaultValue": "member" }, "createdAt": { "type": "date", "required": true } } }, "invitation": { "fields": { "organizationId": { "type": "string", "required": true, "references": { "model": "organization", "field": "id" } }, "email": { "type": "string", "required": true, "sortable": true }, "role": { "type": "string", "required": false, "sortable": true }, "status": { "type": "string", "required": true, "sortable": true, "defaultValue": "pending" }, "expiresAt": { "type": "date", "required": true }, "inviterId": { "type": "string", "references": { "model": "user", "field": "id" }, "required": true } } }, "session": { "fields": { "activeOrganizationId": { "type": "string", "required": false } } } }, "$Infer": { "Organization": {}, "Invitation": {}, "Member": {}, "Team": {}, "TeamMember": {}, "ActiveOrganization": {} } "options": { "membershipLimit": 10, "organizationCreation": { "disabled": false } } } }, { "name": "custom-session", "config": { "id": "custom-session", "hooks": { "after": [ {} ] }, "endpoints": {} } } ], "hooks": {} } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
Author
Owner

@dosubot[bot] commented on GitHub (Nov 19, 2025):

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

Your issue is almost certainly caused by the MagicLink plugin not being properly registered in your Better Auth server config at the point where you create and export your auth object. If the plugin isn't included in the plugins array when you call betterAuth(), the MagicLink API endpoints (like auth.api.signInMagicLink) won't exist, and auth.api will be undefined see code.

In your code sample, you only show emailAndPassword: { enabled: true } in the config, but your system info shows the MagicLink plugin in the config. Double-check that your actual server-side code looks like this:

import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins";

export const auth = betterAuth({
  emailAndPassword: { enabled: true },
  plugins: [
    magicLink({
      sendMagicLink: async ({ email, url, token }) => {
        // send the email here
      }
    })
  ]
});

If you don't pass the plugin to betterAuth(), or if you import/export the wrong auth instance, auth.api will be undefined.

Also, for TypeScript, if you see type errors or missing API methods, use an explicit type annotation when exporting auth:

export const auth = betterAuth(authConfig) as ReturnType<typeof betterAuth<typeof authConfig>>;

If you're in a serverless or edge environment, make sure to instantiate auth inside your request handler, not at the module level, to avoid cold start/plugin registration issues reference.

Finally, in Better Auth v1.3.27, there are known issues with MagicLink plugin registration and API exposure. If you still see problems after fixing your config, check for updates or monitor open PRs for a fix reference.

Let me know if you want a minimal working example or if you still see auth.api as undefined after these changes.

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 (Nov 19, 2025): <!-- Greeting --> Hi @whitetown! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your issue is almost certainly caused by the MagicLink plugin not being properly registered in your Better Auth server config at the point where you create and export your `auth` object. If the plugin isn't included in the `plugins` array when you call `betterAuth()`, the MagicLink API endpoints (like `auth.api.signInMagicLink`) won't exist, and `auth.api` will be undefined [see code](https://github.com/better-auth/better-auth/blob/54d0f8a0304cee0185936abc5b46413b1e8ce3b2/packages/better-auth/src/auth.ts#L19-L61). In your code sample, you only show `emailAndPassword: { enabled: true }` in the config, but your system info shows the MagicLink plugin in the config. Double-check that your actual server-side code looks like this: ```ts import { betterAuth } from "better-auth"; import { magicLink } from "better-auth/plugins"; export const auth = betterAuth({ emailAndPassword: { enabled: true }, plugins: [ magicLink({ sendMagicLink: async ({ email, url, token }) => { // send the email here } }) ] }); ``` If you don't pass the plugin to `betterAuth()`, or if you import/export the wrong `auth` instance, `auth.api` will be undefined. Also, for TypeScript, if you see type errors or missing API methods, use an explicit type annotation when exporting `auth`: ```ts export const auth = betterAuth(authConfig) as ReturnType<typeof betterAuth<typeof authConfig>>; ``` If you're in a serverless or edge environment, make sure to instantiate `auth` inside your request handler, not at the module level, to avoid cold start/plugin registration issues [reference](https://github.com/better-auth/better-auth/issues/969). Finally, in Better Auth v1.3.27, there are known issues with MagicLink plugin registration and API exposure. If you still see problems after fixing your config, check for updates or monitor open PRs for a fix [reference](https://github.com/better-auth/better-auth/issues/5483). Let me know if you want a minimal working example or if you still see `auth.api` as undefined after these changes. <!-- 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/7b4e79b5-705c-436e-bbab-011f2c379475?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/7b4e79b5-705c-436e-bbab-011f2c379475?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/7b4e79b5-705c-436e-bbab-011f2c379475?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/7b4e79b5-705c-436e-bbab-011f2c379475?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/7b4e79b5-705c-436e-bbab-011f2c379475?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/7b4e79b5-705c-436e-bbab-011f2c379475?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/7b4e79b5-705c-436e-bbab-011f2c379475?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/6104)
Author
Owner

@whitetown commented on GitHub (Nov 19, 2025):

Thank you for the quick reply.
I definitely have magic link plugin

plugins: [
        magicLink({
            sendMagicLink: async ({ email, url }) => {
                logger('sendMagicLink', email, url)
                await sendEmail({
                    to: email, 
                    subject: 'Magic Link to XXXX',
                    text: `Click the link to confirm login to XXXX:\n${url}`,
                })
            },
        }),
]

I get emails when I initiate magic link from FE UI.

@whitetown commented on GitHub (Nov 19, 2025): Thank you for the quick reply. I definitely have magic link plugin ``` plugins: [ magicLink({ sendMagicLink: async ({ email, url }) => { logger('sendMagicLink', email, url) await sendEmail({ to: email, subject: 'Magic Link to XXXX', text: `Click the link to confirm login to XXXX:\n${url}`, }) }, }), ] ``` I get emails when I initiate magic link from FE UI.
Author
Owner

@dosubot[bot] commented on GitHub (Nov 19, 2025):

Thanks for confirming the plugin is present and that emails are sent—this proves the magic link plugin is active in at least one environment. However, if auth.api is undefined on the server, it almost always means the server-side Better Auth instance is not being created with the same config (including plugins) as the one handling your frontend/API requests, or you're importing a different auth object in your backend code reference.

A few things to check:

  • Make sure the exact same auth instance (with the magic link plugin in the plugins array) is used everywhere you call auth.api.signInMagicLink on the server.
  • If you have multiple files or environments (like Next.js API routes, serverless functions, or monorepo setups), confirm that you are not accidentally importing a different or incomplete auth object.
  • Avoid type assertions like magicLink() as unknown as BetterAuthPlugin—these can break API exposure.
  • In Next.js/serverless, always instantiate auth inside the handler if you see cold start or plugin registration issues.

If you can share the code where you instantiate and export auth on the server, I can help spot any mismatches. This is a common pitfall in projects where the frontend and backend have separate configs or import paths reference.

Let me know if you want a minimal working server-side example for reference!

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 (Nov 19, 2025): <!-- Answer --> Thanks for confirming the plugin is present and that emails are sent—this proves the magic link plugin is active in at least one environment. However, if `auth.api` is undefined on the server, it almost always means the server-side Better Auth instance is not being created with the same config (including plugins) as the one handling your frontend/API requests, or you're importing a different `auth` object in your backend code [reference](https://github.com/better-auth/better-auth/blob/54d0f8a0304cee0185936abc5b46413b1e8ce3b2/packages/better-auth/src/auth.ts#L19-L74). A few things to check: - Make sure the exact same `auth` instance (with the magic link plugin in the plugins array) is used everywhere you call `auth.api.signInMagicLink` on the server. - If you have multiple files or environments (like Next.js API routes, serverless functions, or monorepo setups), confirm that you are not accidentally importing a different or incomplete `auth` object. - Avoid type assertions like `magicLink() as unknown as BetterAuthPlugin`—these can break API exposure. - In Next.js/serverless, always instantiate `auth` inside the handler if you see cold start or plugin registration issues. If you can share the code where you instantiate and export `auth` on the server, I can help spot any mismatches. This is a common pitfall in projects where the frontend and backend have separate configs or import paths [reference](https://github.com/better-auth/better-auth/issues/3015). Let me know if you want a minimal working server-side example for reference! <!-- 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/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ac7a402c-0e9a-4cfe-8dc3-2878a194590d?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/6104)
Author
Owner

@xkema commented on GitHub (Nov 19, 2025):

Hi @whitetown,

I think the screenshot you shared confused the AI thingie since it didn't mention the Invalid URL error. I've experienced a similar issue as well, it's probably the same one.

In my case, when working in the development environment, the baseURL is set to an empty string "". For some reason server code isn't picking up the variables defined in the .env file.

This is where the error is thrown, and it's most likely a bug since other API endpoints don't throw it. :) (On the other APIs. URL is constructed like, ${baseURL}/some/path/, so empty string input doesn't do any harm on the new URL() constructor since it's not called at all.)

If it's the same issue, the possible fixes are:

  1. In your betterAuth({...}) configuration, manually set the baseURL option to your development URL, e.g., http://localhost:1234. (Only for development environment if possible.)
  2. Add a before hook and override the baseURL option only for ctx.path === "/sign-in/magic-link" requests. (Only for development environment if possible.)

Both are quickfixes, but they might save the day.

Best!

@xkema commented on GitHub (Nov 19, 2025): Hi @whitetown, I think the screenshot you shared confused the AI thingie since it didn't mention the `Invalid URL` error. I've experienced a similar issue as well, it's probably the same one. In my case, when working in the development environment, the `baseURL` is set to an empty string `""`. For some reason server code isn't picking up the variables defined in the `.env` file. [This is](https://github.com/better-auth/better-auth/blob/v1.3.27/packages/better-auth/src/plugins/magic-link/index.ts#L192) where the error is thrown, and it's most likely a bug since other API endpoints don't throw it. :) (On the other APIs. URL is constructed like, `${baseURL}/some/path/`, so empty string input doesn't do any harm on the `new URL()` constructor since it's not called at all.) If it's the same issue, the possible fixes are: 1. In your `betterAuth({...})` configuration, manually set the `baseURL` option to your development URL, e.g., `http://localhost:1234`. (Only for development environment if possible.) 2. Add a [before hook](https://www.better-auth.com/docs/concepts/hooks#before-hooks) and override the `baseURL` option only for `ctx.path === "/sign-in/magic-link"` requests. (Only for development environment if possible.) Both are quickfixes, but they might save the day. Best!
Author
Owner

@whitetown commented on GitHub (Nov 20, 2025):

@xkema
Yes, that helped:

export const auth = betterAuth({
    ...(process.env.NODE_ENV === 'production' ? {} : { baseURL: process.env.NEXT_PUBLIC_APP_URL }),

   //[skipped]

thanks!

@whitetown commented on GitHub (Nov 20, 2025): @xkema Yes, that helped: ``` export const auth = betterAuth({ ...(process.env.NODE_ENV === 'production' ? {} : { baseURL: process.env.NEXT_PUBLIC_APP_URL }), //[skipped] ``` thanks!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2335