BetterAuth GitHub OAuth Creates Only Verification Records, No User/Account/Session

Compiled /auth/signin in 9.6s (9870 modules)
GET /auth/signin 200 in 9866ms
Compiling /api/auth/[...all] ...
Compiled /api/auth/[...all] in 2.5s (5762 modules)
2025-04-27T04:28:04.931Z INFO [Better Auth]: [Drizzle Adapter] #0 [1/4] create (Unsafe Input): {
model: 'verification',
data: {
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z,
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
identifier: '[REDACTED]',
expiresAt: 2025-04-27T04:38:04.931Z
}
}
2025-04-27T04:28:04.932Z INFO [Better Auth]: [Drizzle Adapter] #0 [2/4] create (Parsed Input): {
model: 'verification',
data: {
identifier: '[REDACTED]',
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
expiresAt: 2025-04-27T04:38:04.931Z,
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z,
id: undefined
}
}
Compiled /auth/signin in 9.6s (9870 modules)
GET /auth/signin 200 in 9866ms
Compiling /api/auth/[...all] ...
Compiled /api/auth/[...all] in 2.5s (5762 modules)
2025-04-27T04:28:04.931Z INFO [Better Auth]: [Drizzle Adapter] #0 [1/4] create (Unsafe Input): {
model: 'verification',
data: {
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z,
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
identifier: '[REDACTED]',
expiresAt: 2025-04-27T04:38:04.931Z
}
}
2025-04-27T04:28:04.932Z INFO [Better Auth]: [Drizzle Adapter] #0 [2/4] create (Parsed Input): {
model: 'verification',
data: {
identifier: '[REDACTED]',
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
expiresAt: 2025-04-27T04:38:04.931Z,
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z,
id: undefined
}
}
36 Replies
JROCBABY
JROCBABYOP4d ago
2025-04-27T04:28:04.977Z INFO [Better Auth]: [Drizzle Adapter] #0 [3/4] create (DB Result): {
model: 'verification',
res: {
id: '[REDACTED]',
identifier: '[REDACTED]',
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
expiresAt: 2025-04-27T04:38:04.931Z,
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z
}
}
2025-04-27T04:28:04.978Z INFO [Better Auth]: [Drizzle Adapter] #0 [4/4] create (Parsed Result): {
model: 'verification',
data: {
identifier: '[REDACTED]',
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
expiresAt: 2025-04-27T04:38:04.931Z,
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z,
id: '[REDACTED]'
}
}
POST /api/auth/sign-in/social 200 in 3411ms
Compiling /meal-settings ...
Compiled /meal-settings in 2.4s (10748 modules)
Fetched /api/cooking-times: Error: No active session
2025-04-27T04:28:04.977Z INFO [Better Auth]: [Drizzle Adapter] #0 [3/4] create (DB Result): {
model: 'verification',
res: {
id: '[REDACTED]',
identifier: '[REDACTED]',
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
expiresAt: 2025-04-27T04:38:04.931Z,
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z
}
}
2025-04-27T04:28:04.978Z INFO [Better Auth]: [Drizzle Adapter] #0 [4/4] create (Parsed Result): {
model: 'verification',
data: {
identifier: '[REDACTED]',
value: '{"callbackURL":"http://localhost:4200","codeVerifier":"[REDACTED]","expiresAt":1745728684931}',
expiresAt: 2025-04-27T04:38:04.931Z,
createdAt: 2025-04-27T04:28:04.931Z,
updatedAt: 2025-04-27T04:28:04.931Z,
id: '[REDACTED]'
}
}
POST /api/auth/sign-in/social 200 in 3411ms
Compiling /meal-settings ...
Compiled /meal-settings in 2.4s (10748 modules)
Fetched /api/cooking-times: Error: No active session
It creates no cookie, no session, nothing. Why is it incapable of giving me a useful error?
daveycodez
daveycodez4d ago
Can you share your auth.ts file This is for a new account, a sign up ? First time connect? signIn.social should be redirecting you to GitHub to sign into your GitHub account, then from there redirecting you back to /api/auth/callback/github after you sign in
JROCBABY
JROCBABYOP4d ago
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import {
admin,
captcha,
emailOTP,
haveIBeenPwned,
magicLink,
username,
} from 'better-auth/plugins';
import { passkey } from 'better-auth/plugins/passkey';
import { nextCookies } from 'better-auth/next-js';
import { db } from './schema';
import nodemailer from 'nodemailer';
import {
accountsTable,
passkeysTable,
sessionsTable,
usersTable,
verificationsTable,
} from '@nomyom/nomyom-shared/schema';

export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: {
user: usersTable,
account: accountsTable,
session: sessionsTable,
verification: verificationsTable,
passkey: passkeysTable,
},
debugLogs: true,
}),
advanced: {
database: {
generateId: false,
},
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
},
emailAndPassword: {
enabled: true,
async verifySignIn({
email,
password,
}: {
email: string;
password: string;
}) {
try {
const response = await fetch(
`${process.env.NOMYOM_API_URL}/api/users/login`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}
);
if (!response.ok) {
console.error(
'Login failed:',
response.status,
await response.text()
);
return false;
}
const user = await response.json();
return !!user?.id;
} catch (error) {
console.error('Verify sign-in error:', error);
return false;
}
},
},
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import {
admin,
captcha,
emailOTP,
haveIBeenPwned,
magicLink,
username,
} from 'better-auth/plugins';
import { passkey } from 'better-auth/plugins/passkey';
import { nextCookies } from 'better-auth/next-js';
import { db } from './schema';
import nodemailer from 'nodemailer';
import {
accountsTable,
passkeysTable,
sessionsTable,
usersTable,
verificationsTable,
} from '@nomyom/nomyom-shared/schema';

export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'pg',
schema: {
user: usersTable,
account: accountsTable,
session: sessionsTable,
verification: verificationsTable,
passkey: passkeysTable,
},
debugLogs: true,
}),
advanced: {
database: {
generateId: false,
},
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
},
emailAndPassword: {
enabled: true,
async verifySignIn({
email,
password,
}: {
email: string;
password: string;
}) {
try {
const response = await fetch(
`${process.env.NOMYOM_API_URL}/api/users/login`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}
);
if (!response.ok) {
console.error(
'Login failed:',
response.status,
await response.text()
);
return false;
}
const user = await response.json();
return !!user?.id;
} catch (error) {
console.error('Verify sign-in error:', error);
return false;
}
},
},
plugins: [
admin(),
passkey(),
haveIBeenPwned(),
captcha({
provider: 'cloudflare-turnstile',
secretKey: process.env.TURNSTILE_SECRET_KEY as string,
}),
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

await transporter.sendMail({
from: '"Nomyom" <[email protected]>',
to: email,
subject: `Your Nomyom OTP`,
text: `Your one-time password (OTP) is: ${otp}. It expires in 10 minutes.`,
html: `<p>Your one-time password (OTP) is: <strong>${otp}</strong>. It expires in 10 minutes.</p>`,
});
},
}),
magicLink({
sendMagicLink: async ({ email, token, url }, request) => {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

await transporter.sendMail({
from: '"Nomyom" <[email protected]>',
to: email,
subject: `Your Nomyom Magic Link`,
text: `Click this link to sign in: ${url}. It expires in 15 minutes.`,
html: `<p>Click this link to sign in: <a href="${url}">${url}</a>. It expires in 15 minutes.</p>`,
});
},
}),
username(),
nextCookies(),
],
});
plugins: [
admin(),
passkey(),
haveIBeenPwned(),
captcha({
provider: 'cloudflare-turnstile',
secretKey: process.env.TURNSTILE_SECRET_KEY as string,
}),
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

await transporter.sendMail({
from: '"Nomyom" <[email protected]>',
to: email,
subject: `Your Nomyom OTP`,
text: `Your one-time password (OTP) is: ${otp}. It expires in 10 minutes.`,
html: `<p>Your one-time password (OTP) is: <strong>${otp}</strong>. It expires in 10 minutes.</p>`,
});
},
}),
magicLink({
sendMagicLink: async ({ email, token, url }, request) => {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});

await transporter.sendMail({
from: '"Nomyom" <[email protected]>',
to: email,
subject: `Your Nomyom Magic Link`,
text: `Click this link to sign in: ${url}. It expires in 15 minutes.`,
html: `<p>Click this link to sign in: <a href="${url}">${url}</a>. It expires in 15 minutes.</p>`,
});
},
}),
username(),
nextCookies(),
],
});
daveycodez
daveycodez4d ago
What do you have your GitHub redirect_uri configured to? It your GitHub.com developer settings for your oauth app As for debugLogs, you have it set to true in your drizzleAdapter which enables debug logs for Drizzle queries, but you can additionally pass debugLogs to betterAuth({ }) (the root options) Like this
export const auth = betterAuth({
logger: {
"level": "debug",
},
database: drizzleAdapter(db, {
provider: 'pg',
schema: {
user: usersTable,
account: accountsTable,
session: sessionsTable,
verification: verificationsTable,
passkey: passkeysTable,
},
debugLogs: true,
})
export const auth = betterAuth({
logger: {
"level": "debug",
},
database: drizzleAdapter(db, {
provider: 'pg',
schema: {
user: usersTable,
account: accountsTable,
session: sessionsTable,
verification: verificationsTable,
passkey: passkeysTable,
},
debugLogs: true,
})
Sorry actually it's logger
JROCBABY
JROCBABYOP4d ago
Well I'm trying to sign in with github OAuth, but it's not sending me to github. at all, it just shows the logs from my first post
daveycodez
daveycodez4d ago
Can you show me the code you are using to sign in with GitHub? Are you possibly doing it in a server action?
JROCBABY
JROCBABYOP4d ago
http://localhost:4200/api/auth/callback/github
http://localhost:4200/api/auth/callback/github
my nextjs app runs on 4200
daveycodez
daveycodez4d ago
If you aren't getting redirect to GitHub this would be a client side issue I think, need to see the code where you are invoking signIn.social I see the API is returning a 200 for sign-in/social for you, the authClient should handle redirecting you to GitHub Can you show me the code for your Sign in with GitHub button and the function it calls
JROCBABY
JROCBABYOP4d ago
'use client';

import { GithubLogo } from '@phosphor-icons/react';
import styles from './socialAuth.module.css';
import { authClient } from '../../../lib/authClient';
import { useSignIn } from '../../../providers/auth/signInProvider';

export function SocialAuth({ provider }: { provider: 'github' }) {
const { isLoading, setActiveMethod, setError, setIsLoading, callbackURL } =
useSignIn();

const handleSocialSignIn = async () => {
setIsLoading(true);
setError('');
setActiveMethod('social');
try {
const result = await authClient.signIn.social({ provider });
if (result.error) {
setError(result.error.message || 'Social sign-in failed');
} else {
window.location.href = callbackURL; // Use window.location for client-side redirect
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setIsLoading(false);
}
};

return (
<button
onClick={handleSocialSignIn}
className={`${styles.socialButton} ${styles.githubButton}`}
disabled={isLoading}
>
{isLoading ? (
<span className={styles.spinner} />
) : (
<>
<GithubLogo weight="fill" size={20} />
Sign in with GitHub
</>
)}
</button>
);
}
'use client';

import { GithubLogo } from '@phosphor-icons/react';
import styles from './socialAuth.module.css';
import { authClient } from '../../../lib/authClient';
import { useSignIn } from '../../../providers/auth/signInProvider';

export function SocialAuth({ provider }: { provider: 'github' }) {
const { isLoading, setActiveMethod, setError, setIsLoading, callbackURL } =
useSignIn();

const handleSocialSignIn = async () => {
setIsLoading(true);
setError('');
setActiveMethod('social');
try {
const result = await authClient.signIn.social({ provider });
if (result.error) {
setError(result.error.message || 'Social sign-in failed');
} else {
window.location.href = callbackURL; // Use window.location for client-side redirect
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setIsLoading(false);
}
};

return (
<button
onClick={handleSocialSignIn}
className={`${styles.socialButton} ${styles.githubButton}`}
disabled={isLoading}
>
{isLoading ? (
<span className={styles.spinner} />
) : (
<>
<GithubLogo weight="fill" size={20} />
Sign in with GitHub
</>
)}
</button>
);
}
'use client';

import { GithubLogo } from '@phosphor-icons/react';
import styles from './socialAuth.module.css';
import { authClient } from '../../../lib/authClient';
import { useSignIn } from '../../../providers/auth/signInProvider';

export function SocialAuth({ provider }: { provider: 'github' }) {
const { isLoading, setActiveMethod, setError, setIsLoading, callbackURL } =
useSignIn();

const handleSocialSignIn = async () => {
setIsLoading(true);
setError('');
setActiveMethod('social');
try {
const result = await authClient.signIn.social({ provider });
if (result.error) {
setError(result.error.message || 'Social sign-in failed');
} else {
window.location.href = callbackURL; // Use window.location for client-side redirect
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setIsLoading(false);
}
};

return (
<button
onClick={handleSocialSignIn}
className={`${styles.socialButton} ${styles.githubButton}`}
disabled={isLoading}
>
{isLoading ? (
<span className={styles.spinner} />
) : (
<>
<GithubLogo weight="fill" size={20} />
Sign in with GitHub
</>
)}
</button>
);
}
'use client';

import { GithubLogo } from '@phosphor-icons/react';
import styles from './socialAuth.module.css';
import { authClient } from '../../../lib/authClient';
import { useSignIn } from '../../../providers/auth/signInProvider';

export function SocialAuth({ provider }: { provider: 'github' }) {
const { isLoading, setActiveMethod, setError, setIsLoading, callbackURL } =
useSignIn();

const handleSocialSignIn = async () => {
setIsLoading(true);
setError('');
setActiveMethod('social');
try {
const result = await authClient.signIn.social({ provider });
if (result.error) {
setError(result.error.message || 'Social sign-in failed');
} else {
window.location.href = callbackURL; // Use window.location for client-side redirect
}
} catch (err) {
setError('An unexpected error occurred');
} finally {
setIsLoading(false);
}
};

return (
<button
onClick={handleSocialSignIn}
className={`${styles.socialButton} ${styles.githubButton}`}
disabled={isLoading}
>
{isLoading ? (
<span className={styles.spinner} />
) : (
<>
<GithubLogo weight="fill" size={20} />
Sign in with GitHub
</>
)}
</button>
);
}
daveycodez
daveycodez4d ago
is the signInProvider a custom provider you made? a ContextProvider it looks like from here
JROCBABY
JROCBABYOP4d ago
yeah
daveycodez
daveycodez4d ago
You shouldn't need to try { catch } the signIn.social, since you aren't passing fetchOptions: {throw: true} to it So you can remove that try catch block I see what is happening though You are calling window.location.href = "callback" yourself, get rid of that your window.location.href there is overriding the authClient's So it is going directly to your callbackURL which is /meals instead of GitHub You're not seeing the logs in the API because this is a client side issue here, and also there are no errors from Better Auth which is why it isn't logging any, here I will show you how to fix
const handleSocialSignIn = async () => {
setIsLoading(true);
setError('');
setActiveMethod('social');

const { error } = await authClient.signIn.social({ provider, callbackURL });

if (error) {
setError(error.message || 'Social sign-in failed');
}

setIsLoading(false);
}
const handleSocialSignIn = async () => {
setIsLoading(true);
setError('');
setActiveMethod('social');

const { error } = await authClient.signIn.social({ provider, callbackURL });

if (error) {
setError(error.message || 'Social sign-in failed');
}

setIsLoading(false);
}
This should work
JROCBABY
JROCBABYOP4d ago
yeah it works now ty
daveycodez
daveycodez4d ago
You pass your callbackURL to social({}) and it will redirect you there after sign up sure thing
JROCBABY
JROCBABYOP4d ago
Thank you so much, I would've never found this myself tbh since there wasn't any error to go on really
daveycodez
daveycodez4d ago
No prob lucky I opened my laptop just now heh. But yea, the authClient.signIn.social method will handle redirects for you automatically
JROCBABY
JROCBABYOP4d ago
Yeah I missed that, but thanks again, I was pulling my hair out. Is there anywhere I can see the schema's better auth expects? Because it generated this
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
role: text('role'),
banned: boolean('banned'),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires'),
username: text('username').unique(),
displayUsername: text('display_username'),
});
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
role: text('role'),
banned: boolean('banned'),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires'),
username: text('username').unique(),
displayUsername: text('display_username'),
});
At least, that's what I think, but now I'm getting
PostgresError: null value in column "username" of relation "users" violates not-null constraint
2025-04-27T05:14:31.420Z ERROR [Better Auth]: unable_to_create_user
PostgresError: null value in column "username" of relation "users" violates not-null constraint
2025-04-27T05:14:31.420Z ERROR [Better Auth]: unable_to_create_user
daveycodez
daveycodez4d ago
Oh.. you're using GitHub sign in + username plugin, interesting I think GitHub is trying to sign up a user with null as the username Try adding .notNull() to username wait no dont do that one sec
JROCBABY
JROCBABYOP4d ago
No the issue is the unique constraint, I think that forces it to be not null, since otherwise if you'd have multiple null records, that would make it not unique
daveycodez
daveycodez4d ago
Yea one sec I don't know if username plugin is compatible with Github sign in out of the box. Cause in this case you want it to be null I think you need to generate a random username for them
JROCBABY
JROCBABYOP4d ago
Yeah maybe infer it from their email or something
daveycodez
daveycodez4d ago
and populate it in the before hook for the database One sec can you paste your full schema That unique() constraint should allow null Is it possible your database schema actually has NOT NULL applied to username Or some other relation is checking username So yea I would say to run the database migrations again, paste your full schema to me, then do a drizzle-kit pull and paste the schema it pulls down separately
JROCBABY
JROCBABYOP4d ago
wait you're right, when I reran the migration the null constraint disappeared, sorry!
daveycodez
daveycodez4d ago
perfect
JROCBABY
JROCBABYOP4d ago
one last thing, in the signInProvider, I also do a router.push(callbackURL); is that also a bad idea?
daveycodez
daveycodez4d ago
nah you don't need to do that for oauth it'll automatically redirect to callbackURL you pass to signIn.social you can do that router push after an email/pass sign in tho
JROCBABY
JROCBABYOP4d ago
I'm running into an issue where the client sends a token like xxxxxxxxxxx(dot)xxxxxxxxxxxxx but the sessions table only has the token part before the dot, this leads to 401 errors, any idea why the client sends xxxxxxxxxxx(dot)xxxxxxxxxxxxx?
daveycodez
daveycodez4d ago
I'm not quite sure whats the feature that's not working
JROCBABY
JROCBABYOP4d ago
The client sends a token that includes an additional encoded segment (e.g., abc123.xyz789), while the database stores only the base part (e.g., abc123). This causes the server to reject the token as invalid, resulting in a 401 Unauthorized error. That's how to cookie is saved in the browser, abc123.xyz789, but when I look in the db the session token column only contains abc123
daveycodez
daveycodez4d ago
What is sending this token Like what feature isn't working Sign in? sign up? email verification? social sign in? Or nothing
JROCBABY
JROCBABYOP4d ago
Better auth sets a cookie with token abc123.xyz789 in the browser. But in the sessions table the token only consists of abc123. So when I send a cookie to the backend using withCredentials using axios or something and compare it with the token in the sessions table they don't match up
daveycodez
daveycodez3d ago
Yea I have no idea I'm not using axios at all and IDK why it would be storing it like that with your setup Try not using a cookie prefix
JROCBABY
JROCBABYOP3d ago
But that has nothing to do with axios? It's literally how better-auth inserts the cookie into the browser. If I use cookies() it retrieves abc123.xyz789 from the browser, and that's how I can see it in the developer tools > application > cookies as well
daveycodez
daveycodez3d ago
IDK I would make a separate post about this cause I'm not sure why it's doing that for you. Maybe someone else will see it, include your auth.ts and any related functions in that post
JROCBABY
JROCBABYOP3d ago
Okay thanks bud, I asked around in the general, but I'll make a new help post. Appreciate the help.

Did you find this page helpful?