diff --git a/docs/components/sidebar-content.tsx b/docs/components/sidebar-content.tsx
index 82c4b4ba..87b488d7 100644
--- a/docs/components/sidebar-content.tsx
+++ b/docs/components/sidebar-content.tsx
@@ -5,6 +5,7 @@ import {
Mailbox,
Phone,
ScanFace,
+ UserCircle,
Users2,
UserSquare2,
} from "lucide-react";
@@ -582,6 +583,11 @@ export const contents: Content[] = [
icon: UserSquare2,
href: "/docs/plugins/username",
},
+ {
+ title: "Anonymous",
+ icon: UserCircle,
+ href: "/docs/plugins/anonymous",
+ },
{
title: "Phone Number",
icon: Phone,
@@ -609,6 +615,7 @@ export const contents: Content[] = [
),
},
+
{
title: "Authorization",
group: true,
diff --git a/docs/content/docs/plugins/anonymous.mdx b/docs/content/docs/plugins/anonymous.mdx
new file mode 100644
index 00000000..48b67846
--- /dev/null
+++ b/docs/content/docs/plugins/anonymous.mdx
@@ -0,0 +1,94 @@
+---
+title: Aanonymous
+description: Anonymous plugin for Better Auth.
+---
+
+Anonymous plugin allows you to provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, the user can link an authentication method to their account.
+
+
+## Installation
+
+
+
+ ### Add the plugin to your auth config
+
+ Add the anonymous plugin to your auth config.
+
+ ```ts title="auth.ts"
+ import { betterAuth } from "better-auth"
+ import { anonymous } from "better-auth/plugins" // [!code highlight]
+
+ export const auth = await betterAuth({
+ // ... other config options
+ plugins: [
+ anonymous() // [!code highlight]
+ ]
+ })
+ ```
+
+
+ ### Migrate your database:
+ Run migrate or generate to create the required tables in your database.
+
+ ```bash title="terminal"
+ npx better-auth migrate # [!code highlight]
+ ```
+
+ or
+
+ ```bash title="terminal"
+ npx better-auth generate
+ ```
+
+ See the [schema-section](#schema) to see what fields this plugin requires. You can also add these fields manually to your database.
+
+
+
+ ### Add the client Plugin
+
+ Add the client plugin in your auth client instance.
+
+ ```ts title="client.ts"
+ import { createAuthClient } from "better-auth/client"
+ import { anonymousClient } from "better-auth/client/plugins"
+
+ const client = createAuthClient({
+ plugins: [
+ anonymousClient()
+ ]
+ })
+ ```
+
+
+
+
+## Usage
+
+### Sign In
+
+To sign in a user anonymously, call the `signIn.anonymously` method.
+
+```ts title="example.ts"
+const user = await client.signIn.anonymous()
+```
+
+### Link Account
+
+once the user is signed in, you can link an account to the user. Currenyly, only email/password is supported.
+
+```ts title="example.ts"
+const user = await client.anonymous.linkAccount({
+ email: "user@email.com",
+ password: "secure-password"
+})
+```
+
+## Schema
+
+The anonymous plugin requires one additinal field in the user table.
+
+
diff --git a/packages/better-auth/src/client/plugins/index.ts b/packages/better-auth/src/client/plugins/index.ts
index a3b65429..2ad2e5f9 100644
--- a/packages/better-auth/src/client/plugins/index.ts
+++ b/packages/better-auth/src/client/plugins/index.ts
@@ -5,3 +5,4 @@ export * from "../../plugins/two-factor/client";
export * from "../../plugins/passkey/client";
export * from "../../plugins/magic-link/client";
export * from "../../plugins/phone-number/client";
+export * from "../../plugins/anonymous/client";
diff --git a/packages/better-auth/src/db/internal-adapter.ts b/packages/better-auth/src/db/internal-adapter.ts
index 3a63d364..fcad9aac 100644
--- a/packages/better-auth/src/db/internal-adapter.ts
+++ b/packages/better-auth/src/db/internal-adapter.ts
@@ -31,7 +31,7 @@ export const createInternalAdapter = (
return null;
}
},
- createUser: async (user: User) => {
+ createUser: async (user: User & Record) => {
const createdUser = await createWithHooks(user, "user");
return createdUser;
},
diff --git a/packages/better-auth/src/plugins/anonymous/anon.test.ts b/packages/better-auth/src/plugins/anonymous/anon.test.ts
new file mode 100644
index 00000000..d62a4f2e
--- /dev/null
+++ b/packages/better-auth/src/plugins/anonymous/anon.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from "vitest";
+import { anonymous } from ".";
+import { getTestInstance } from "../../test-utils/test-instance";
+import { createAuthClient } from "../../client";
+import { anonymousClient } from "./client";
+
+describe("anonymous", async () => {
+ const { customFetchImpl, sessionSetter } = await getTestInstance({
+ plugins: [anonymous()],
+ });
+ const headers = new Headers();
+ const client = createAuthClient({
+ plugins: [anonymousClient()],
+ fetchOptions: {
+ customFetchImpl,
+ headers,
+ },
+ baseURL: "http://localhost:3000",
+ });
+
+ it("should sign in anonymously", async () => {
+ const anonUser = await client.signIn.anonymous({
+ fetchOptions: {
+ onSuccess: sessionSetter(headers),
+ },
+ });
+ const userId = anonUser.data?.user.id;
+ const isAnonymous = anonUser.data?.user.isAnonymous;
+ const sessionId = anonUser.data?.session.id;
+ expect(userId).toBeDefined();
+ expect(isAnonymous).toBeTruthy();
+ expect(sessionId).toBeDefined();
+ });
+ it("link anonymous user account", async () => {
+ const linkedAccount = await client.user.linkAnonymous({
+ email: "valid-email@email.com",
+ password: "valid-password",
+ });
+ expect(linkedAccount.data?.user).toBeDefined();
+ expect(linkedAccount.data?.session).toBeDefined();
+ });
+
+ it("should sign in after link", async () => {
+ const anonUser = await client.signIn.email({
+ email: "valid-email@email.com",
+ password: "valid-password",
+ });
+ expect(anonUser.data?.user.id).toBeDefined();
+ });
+});
diff --git a/packages/better-auth/src/plugins/anonymous/client.ts b/packages/better-auth/src/plugins/anonymous/client.ts
new file mode 100644
index 00000000..b9a2d601
--- /dev/null
+++ b/packages/better-auth/src/plugins/anonymous/client.ts
@@ -0,0 +1,12 @@
+import type { anonymous } from ".";
+import type { BetterAuthClientPlugin } from "../../client/types";
+
+export const anonymousClient = () => {
+ return {
+ id: "anonymous",
+ $InferServerPlugin: {} as ReturnType,
+ pathMethods: {
+ "/sign-in/anonymous": "POST",
+ },
+ } satisfies BetterAuthClientPlugin;
+};
diff --git a/packages/better-auth/src/plugins/anonymous/index.ts b/packages/better-auth/src/plugins/anonymous/index.ts
new file mode 100644
index 00000000..9b9b6818
--- /dev/null
+++ b/packages/better-auth/src/plugins/anonymous/index.ts
@@ -0,0 +1,129 @@
+import { createAuthEndpoint, sessionMiddleware } from "../../api";
+import { alphabet, generateRandomString } from "../../crypto/random";
+import type { BetterAuthPlugin } from "../../types";
+import { setSessionCookie } from "../../utils/cookies";
+import { z } from "zod";
+import { generateId } from "../../utils/id";
+export const anonymous = () => {
+ return {
+ id: "anonymous",
+ endpoints: {
+ signInAnonymous: createAuthEndpoint(
+ "/sign-in/anonymous",
+ {
+ method: "POST",
+ },
+ async (ctx) => {
+ const tempEmail =
+ "temporary-" + Date.now().toString() + "-better-auth@email.com";
+ const newUser = await ctx.context.internalAdapter.createUser({
+ id: generateId(),
+ email: tempEmail,
+ emailVerified: false,
+ isAnonymous: true,
+ name: "Anonymous",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ });
+ if (!newUser) {
+ return ctx.json(null, {
+ status: 500,
+ body: {
+ message: "Failed to create user",
+ status: 500,
+ },
+ });
+ }
+ const session = await ctx.context.internalAdapter.createSession(
+ newUser.id,
+ ctx.request,
+ );
+ if (!session) {
+ return ctx.json(null, {
+ status: 400,
+ body: {
+ message: "Could not create session",
+ },
+ });
+ }
+ await setSessionCookie(ctx, session.id);
+ return ctx.json({ user: newUser, session });
+ },
+ ),
+ linkAnonymous: createAuthEndpoint(
+ "/user/link-anonymous",
+ {
+ method: "POST",
+ body: z.object({
+ email: z.string().email().optional(),
+ password: z.string().min(6),
+ }),
+ use: [sessionMiddleware],
+ },
+ async (ctx) => {
+ const userId = ctx.context.session.user.id;
+ const { email, password } = ctx.body;
+ let updatedUser = null;
+ // handling both the email - password and updating the user
+ if (email && password) {
+ updatedUser = await ctx.context.internalAdapter.updateUser(userId, {
+ email: email,
+ });
+ }
+ if (!updatedUser) {
+ return ctx.json(null, {
+ status: 500,
+ body: {
+ message: "Failed to update user",
+ status: 500,
+ },
+ });
+ }
+ const hash = await ctx.context.password.hash(password);
+ const updateUserAccount =
+ await ctx.context.internalAdapter.linkAccount({
+ id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
+ userId: updatedUser.id,
+ providerId: "credential",
+ password: hash,
+ accountId: updatedUser.id,
+ });
+ if (!updateUserAccount) {
+ return ctx.json(null, {
+ status: 500,
+ body: {
+ message: "Failed to update account",
+ status: 500,
+ },
+ });
+ }
+ const session = await ctx.context.internalAdapter.createSession(
+ updatedUser.id,
+ ctx.request,
+ );
+ if (!session) {
+ return ctx.json(null, {
+ status: 400,
+ body: {
+ message: "Could not create session",
+ },
+ });
+ }
+ await setSessionCookie(ctx, session.id);
+ return ctx.json({ session, user: updatedUser });
+ },
+ ),
+ },
+ schema: {
+ user: {
+ fields: {
+ isAnonymous: {
+ type: "boolean",
+ defaultValue: true,
+ required: false,
+ },
+ },
+ },
+ },
+ } satisfies BetterAuthPlugin;
+};
diff --git a/packages/better-auth/src/plugins/index.ts b/packages/better-auth/src/plugins/index.ts
index 32869b2f..00f458fb 100644
--- a/packages/better-auth/src/plugins/index.ts
+++ b/packages/better-auth/src/plugins/index.ts
@@ -8,3 +8,4 @@ export * from "../api/call";
export * from "../utils/hide-metadata";
export * from "./magic-link";
export * from "./phone-number";
+export * from "./anonymous";