BA
Better Authβ€’2w ago
Felix

Session cookie not working in production deployment

Hello everyone, Unfortunately, my authentication only works on localhost and not in production or the deployed development environment. We have multiple frontends. Central authentication runs via an Express API (api-ts.playin.gg). The test frontend for authentication is auth.playin.gg. Signing in via Google works; you are redirected to /protected and the cookie is set, but the frontend apparently can't retrieve the sessions. What could be the reason for this? Can anyone help me here? I'm also open to any suggestions for improvement πŸ™‚ auth.ts (Express API)
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
Frontend (auth.playin.gg/protected, NextJS)
import AccountList from "@/components/settings/account-list";
import LogoutButton from "@/components/auth/logout-button";
import MFASection from "@/components/settings/mfa-section";
import PasskeyList from "@/components/settings/passkey-list";
import SessionList from "@/components/settings/session-list";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

const ProtectedSSRPage = async () => {
const { data: session } = await auth.getSession({
fetchOptions: {
headers: await headers(),
},
});

if (!session) {
return redirect("/auth/login");
}

return(...)
}

export default ProtectedSSRPage;
import AccountList from "@/components/settings/account-list";
import LogoutButton from "@/components/auth/logout-button";
import MFASection from "@/components/settings/mfa-section";
import PasskeyList from "@/components/settings/passkey-list";
import SessionList from "@/components/settings/session-list";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

const ProtectedSSRPage = async () => {
const { data: session } = await auth.getSession({
fetchOptions: {
headers: await headers(),
},
});

if (!session) {
return redirect("/auth/login");
}

return(...)
}

export default ProtectedSSRPage;
No description
81 Replies
KiNFiSH
KiNFiSHβ€’2w ago
you mean session is returning null ? like can you check up on the cookie exists with the headers
Felix
FelixOPβ€’2w ago
Yes, if I log the headers, the cookie is present
const heads = await headers();
console.log("HEADERS", heads.get("cookie"));
const heads = await headers();
console.log("HEADERS", heads.get("cookie"));
OUTPUT: HEADERS __Secure-playingg_gguardian.session_token=xxx; ... SESSION null
KiNFiSH
KiNFiSHβ€’2w ago
const session = await auth.api.getSession({
headers: await headers(),
})
const session = await auth.api.getSession({
headers: await headers(),
})
can you do the fetching with the internal api like this
Felix
FelixOPβ€’2w ago
No, unfortunately not. I've separated the frontend and backend. The internal API is therefore in the Express API. I only have the client in NextJS. This prevents access to the internal API. Fetching with the headers or useSession worked on localhost.
import { passkeyClient, twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";

export const auth = createAuthClient({
baseURL: "https://api-ts.playin.gg",
plugins: [passkeyClient(), twoFactorClient()],
});
import { passkeyClient, twoFactorClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";

export const auth = createAuthClient({
baseURL: "https://api-ts.playin.gg",
plugins: [passkeyClient(), twoFactorClient()],
});
KiNFiSH
KiNFiSHβ€’2w ago
Oh if u r using express Ig you should be fine Can you manually pass the cookie to headers options itself
Felix
FelixOPβ€’2w ago
Hmm, you mean read the specific cookie and then pass it in the fetchoptions as headers: { cookie: ... } I don't know if this is intended, and it's also a bit cumbersome. I don't really understand what's wrong with it πŸ˜… I also didn't really find any documentation deployment specific
KiNFiSH
KiNFiSHβ€’2w ago
can u pls share the root page and initialization of your experess app ?
Felix
FelixOPβ€’2w ago
Sure EXPRESS src/index.ts
import v2Routes from "@/routes/v2.routes";
import applyRelations from "@/utils/relations";
import { toNodeHandler } from "better-auth/node";
import bodyParser from "body-parser";
import compression from "compression";
import cookieParser from "cookie-parser";
import cors from "cors";
import dotenv from "dotenv";
import express, { Express, Request, Response } from "express";
import { auth } from "./auth";

dotenv.config({ path: "./.env" });

const app: Express = express();
const baseUrl = process.env.BASE_URL || "http://localhost";
const port = process.env.PORT || 3000;

// CORS
app.use(
cors({
origin: ["https://auth.playin.gg", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "http://localhost:3000"], // @WARN: http:localhost:3000 only for development purposes
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
})
);

// Better-Auth
app.all("/api/auth/*", toNodeHandler(auth));

// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Sequelize - Apply relations
applyRelations();

// Routes (v2)
app.use("/v2", v2Routes);

app.listen(port, () => {
console.log(`🟒 [PlayinGG API]: Running at ${baseUrl}:${port}`);
});
import v2Routes from "@/routes/v2.routes";
import applyRelations from "@/utils/relations";
import { toNodeHandler } from "better-auth/node";
import bodyParser from "body-parser";
import compression from "compression";
import cookieParser from "cookie-parser";
import cors from "cors";
import dotenv from "dotenv";
import express, { Express, Request, Response } from "express";
import { auth } from "./auth";

dotenv.config({ path: "./.env" });

const app: Express = express();
const baseUrl = process.env.BASE_URL || "http://localhost";
const port = process.env.PORT || 3000;

// CORS
app.use(
cors({
origin: ["https://auth.playin.gg", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "http://localhost:3000"], // @WARN: http:localhost:3000 only for development purposes
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
})
);

// Better-Auth
app.all("/api/auth/*", toNodeHandler(auth));

// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Sequelize - Apply relations
applyRelations();

// Routes (v2)
app.use("/v2", v2Routes);

app.listen(port, () => {
console.log(`🟒 [PlayinGG API]: Running at ${baseUrl}:${port}`);
});
EXPRESS src/auth.ts
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
import { betterAuth } from "better-auth";
import { jwt, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import { createPool } from "mysql2/promise";

export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg_gguardian",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
/* await sendEmail({
to: user.email,
subject: "Verify your email address",
text: `Click the link to verify your email: ${url}`,
}); */
console.log(user, url, token);
},
},
socialProviders: {
discord: {
clientId: process.env.DISCORD_CLIENT_ID as string,
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
twitch: {
clientId: process.env.TWITCH_CLIENT_ID as string,
clientSecret: process.env.TWITCH_CLIENT_SECRET as string,
},
},
user: {
modelName: "users",
fields: {
name: "name",
email: "email",
emailVerified: "email_verified",
image: "image",
createdAt: "created_at",
updatedAt: "updated_at",
},
},
database: createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
}),
trustedOrigins: ["http://localhost:3000", "https://www.playin.gg", "https://playin.gg", "https://portal.playin.gg", "https://auth.playin.gg"], // @WARN: http:localhost:3000 only for development purposes
/* session: {
expiresIn: 60,
}, */
plugins: [jwt(), passkey(), twoFactor()],
});
router.get("/", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});

if (!session) {
res.status(401).json({ error: "Unauthorized" });
return;
}

res.json(session);
return;
});
router.get("/", async (req, res) => {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});

if (!session) {
res.status(401).json({ error: "Unauthorized" });
return;
}

res.json(session);
return;
});
I created a route in the Express API as a test. Authentication with the cookie set trough the frontend sign in works. It seems that the problem is only in the frontend in NextJS, where the cookie isn't recognized via the useSession or getSession hook, even though it exists in the await headers.
KiNFiSH
KiNFiSHβ€’2w ago
What is your nextjs version ?
Felix
FelixOPβ€’2w ago
Running latest packages "next": "15.3.0", "react": "^19.1.0",
Felix
FelixOPβ€’2w ago
Also passkeys are not working in production for me, localhost works fine 🫠
onClick={async () => {
await auth.signIn.passkey({
fetchOptions: {
onSuccess: () => {
router.push("/protected");
},
onError: (error) => {
console.log(error);
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
});
}}
onClick={async () => {
await auth.signIn.passkey({
fetchOptions: {
onSuccess: () => {
router.push("/protected");
},
onError: (error) => {
console.log(error);
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
});
}}
# SERVER_ERROR: MissingWebCrypto: An instance of the Crypto API could not be located
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:35:23
7|playingg-api-ts | at new Promise (<anonymous>)
7|playingg-api-ts | at getWebCrypto (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:21:23)
7|playingg-api-ts | at Object.getRandomValues (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getRandomValues.js:11:64)
7|playingg-api-ts | at generateChallenge (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/generateChallenge.js:19:32)
7|playingg-api-ts | at Object.generateAuthenticationOptions (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/authentication/generateAuthenticationOptions.js:19:94)
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/plugins/passkey/index.cjs:369:41
7|playingg-api-ts | at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
7|playingg-api-ts | at async internalHandler (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-call/dist/index.cjs:606:22)
7|playingg-api-ts | at async api.<computed> (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/api/index.cjs:484:22)
# SERVER_ERROR: MissingWebCrypto: An instance of the Crypto API could not be located
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:35:23
7|playingg-api-ts | at new Promise (<anonymous>)
7|playingg-api-ts | at getWebCrypto (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getWebCrypto.js:21:23)
7|playingg-api-ts | at Object.getRandomValues (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/getRandomValues.js:11:64)
7|playingg-api-ts | at generateChallenge (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/helpers/generateChallenge.js:19:32)
7|playingg-api-ts | at Object.generateAuthenticationOptions (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/@simplewebauthn/server/script/authentication/generateAuthenticationOptions.js:19:94)
7|playingg-api-ts | at /home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/plugins/passkey/index.cjs:369:41
7|playingg-api-ts | at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
7|playingg-api-ts | at async internalHandler (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-call/dist/index.cjs:606:22)
7|playingg-api-ts | at async api.<computed> (/home/playingg/actions-runner/_work/playingg-api-ts/playingg-api-ts/node_modules/better-auth/dist/api/index.cjs:484:22)
No description
bekacru
bekacruβ€’2w ago
if you have crosssubdomain cookie setup remove defaultCookieAttributes
Felix
FelixOPβ€’2w ago
I tried it. Unfortunately, it doesn't change the existing problems with retrieving the session or passkey sign in
bekacru
bekacruβ€’2w ago
after the user authenticated on auth.playing.gg where do they get redirected to?
Felix
FelixOPβ€’2w ago
At the moment, I'm using auth.playin.gg as a pure development environment. It redirects you to /protected, which works, both on localhost and deployed. But in "production" it redirects you back to /auth/login, because it doesn't recognize the session, as described above. Headers are present.
<Button
onClick={() =>
auth.signIn.social({
provider: "google",
callbackURL: redirectURL ? `${window.location.origin}${redirectURL}` : `${window.location.origin}/protected`,
fetchOptions: {
onError: (error) => {
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
})
}
className="flex w-full flex-row items-center justify-center space-x-2 rounded-full border border-playingg-gray-700 bg-transparent px-5 py-2 text-base text-white"
>
<Image src="/images/providers/GOOGLE_LOGO.svg" width={20} height={20} alt="Google Icon" />
<span>Sign in with Google</span>
</Button>
<Button
onClick={() =>
auth.signIn.social({
provider: "google",
callbackURL: redirectURL ? `${window.location.origin}${redirectURL}` : `${window.location.origin}/protected`,
fetchOptions: {
onError: (error) => {
if (error.error.status === 0) {
toast.error("Backend is not reachable. Please try again later.");
} else {
toast.error("Something went wrong. Please try again later.");
}
},
},
})
}
className="flex w-full flex-row items-center justify-center space-x-2 rounded-full border border-playingg-gray-700 bg-transparent px-5 py-2 text-base text-white"
>
<Image src="/images/providers/GOOGLE_LOGO.svg" width={20} height={20} alt="Google Icon" />
<span>Sign in with Google</span>
</Button>
We will have several frontends later, as well as an Expo App
bekacru
bekacruβ€’2w ago
could you check if the cookie is being properly set in the browser and if it's sent with the request in the middleware?
Felix
FelixOPβ€’2w ago
We actually don't use auth middleware. And we would like to migrate from Next-Auth. Since only a few pages are fully protected, we would query and redirect the session accordingly in the layouts/pages. Or we would only check whether the user is logged in when certain features are clicked. So far, we've done this with Next-Auth's useSession/getServerSideSession. But it would be the same process as implemented above on the ProtectedSSRPage. As far as I understand, the cookies are definitely present, but not recognized by useSession / getSession on the authClient. I only have the client, since the auth.ts is on Express. Here I had logged the cookies
bekacru
bekacruβ€’2w ago
just to be sure remove the cookie prefix as well and on prod inforce useSecureCookies by setting it to true in the config
Felix
FelixOPβ€’2w ago
Hmm, I tried that. It's also deployed in auth.playin.gg. But it still doesn't work. I don't understand it somehow
appName: "PlayinGG",
advanced: {
// cookiePrefix: "playingg",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
/* defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
}, */
useSecureCookies: true,
},
appName: "PlayinGG",
advanced: {
// cookiePrefix: "playingg",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
/* defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
}, */
useSecureCookies: true,
},
daanish
daanishβ€’2w ago
faced the same problem in production it sets the cookie in browser after login but after redirect, it clear the the cookie in browser which redirect back to login page i found the problem is in middleware because it uses fetch this is the error i get in console
Session verification error: Error: fetch failed
client:start: at context.fetch (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/context.js:303:38)
client:start: at tv (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:39895)
client:start: at async tm (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:41964)
client:start: at async tP (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:42978)
client:start: at async handler (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:43639)
client:start: at async /home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:32351
client:start: at async e4 (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:29211)
client:start: at async /home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:122:26
client:start: at async runWithTaggedErrors (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:119:9)
client:start: at async NextNodeServer.runMiddleware (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/next-server.js:1008:24) {
client:start:
client:start: }
server:start: Listening at http://localhost:4000
server:start: Redis Client Connected
media:start: Listening at http://localhost:5000
Session verification error: Error: fetch failed
client:start: at context.fetch (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/context.js:303:38)
client:start: at tv (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:39895)
client:start: at async tm (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:41964)
client:start: at async tP (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:42978)
client:start: at async handler (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:43639)
client:start: at async /home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:32351
client:start: at async e4 (/home/daanish2003/roro-ai/apps/client/.next/server/src/middleware.js:13:29211)
client:start: at async /home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:122:26
client:start: at async runWithTaggedErrors (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/web/sandbox/sandbox.js:119:9)
client:start: at async NextNodeServer.runMiddleware (/home/daanish2003/roro-ai/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/next-server.js:1008:24) {
client:start:
client:start: }
server:start: Listening at http://localhost:4000
server:start: Redis Client Connected
media:start: Listening at http://localhost:5000
i got this error when i run in prod in local machine i have a backend in expressjs and nextjs as frontend @bekacru have any solution i have detected is problem is useSecureCookie in prod i deployed in aws with nginix it doesn't work
Felix
FelixOPβ€’2w ago
Hmm, I'm also using NGINX as a reverse proxy. I was wondering if that could be the problem, but according to the logs, the cookies are present on the backend. I've already played around with the configuration a bit, but so far, I've never had any problems with cookies... but the better-auth session hooks in my deployed frontend still don't want to recognize the session, even though the cookie is present 😦 I still haven't found a solution.
Fall
Fallβ€’2w ago
Same issue, the cookie is set when sign-in but get-session call remove it
Felix
FelixOPβ€’2w ago
It's actually not even removed for me. I still have the cookie, but it's just not recognized.
Fall
Fallβ€’2w ago
the response of get-session call, why return 3 null cookies?
No description
bekacru
bekacruβ€’2w ago
what is your baseURL or BETTER_AUTH_URL set to? it's trying to unset cookies
Fall
Fallβ€’2w ago
yes, the request does send a valid cookie. I suspect the issue might be related to the session database schema
bekacru
bekacruβ€’2w ago
is this on prod or local?
Fall
Fallβ€’2w ago
prod
bekacru
bekacruβ€’2w ago
what is your base url set to?
Fall
Fallβ€’2w ago
my API runs on a subdomain, api.osler, while my Next.js app runs on the base domain import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import * as schema from '@/db/schema';
import { db } from '@/db'; import { env } from '@/env'; export const auth = betterAuth({ secret: env.BETTER_AUTH_SECRET, baseUrl: env.BETTER_AUTH_URL, database: drizzleAdapter(db, { provider: 'pg', schema: schema, }), user: { modelName: 'doctor', }, account: { fields: { userId: 'doctorId', } }, session: { fields: { userId: 'doctorId', }, storeSessionInDatabase: true, }, emailAndPassword: { enabled: true, async sendResetPassword(data, request) { console.log(data, request); }, }, advanced: { crossSubDomainCookies: { enabled: true, domain: '.osler.app', }, useSecureCookies: true, }, appName: 'Osler', trustedOrigins: ['http://localhost:3001', 'https://osler.app'], socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, } } });
bekacru
bekacruβ€’2w ago
first baseUrl should be baseURL
Fall
Fallβ€’2w ago
oh 😳 I miss type safety right now
bekacru
bekacruβ€’2w ago
will be added on the next release. Extra values aren't being validated by ts cause of generics 🫑
Felix
FelixOPβ€’2w ago
Mine is BETTER_AUTH_URL=https://api-ts.playin.gg Frontend: https://auth.playin.gg But @Fall don't seem to have exact the same problem
PlayinGG | Authentication
PlayinGG Auth Service
bekacru
bekacruβ€’2w ago
if you're not incase enforcing useSecureCookies on prod do that when it's behind reverse proxy, it may not enable it by default
Felix
FelixOPβ€’2w ago
I had already tried this on your suggestion, unfortunately it didn't change anything
bekacru
bekacruβ€’2w ago
so the issue is after sign in and properly seting cookies getSession returns null, right?
Fall
Fallβ€’2w ago
The cookie clearing during the get-session call continues, and the baseURL mismatch wasn’t the problem. It’s the same issue as @daanish
daanish
daanishβ€’2w ago
yes
Felix
FelixOPβ€’2w ago
Yeah, exactly. The cookie is set correctly for me, but getSession and useSession only return null in PROD. On localhost, everything works perfectly. After signing in, you're redirected to /protected, but then, of course, you're redirected directly back to signin because the session is null.
No description
daanish
daanishβ€’2w ago
same after redirect cookie is being cleared
bekacru
bekacruβ€’2w ago
just to make sure, are you all in the latest version of the library?
Fall
Fallβ€’2w ago
i'm using: "better-auth": "^1.2.5",
daanish
daanishβ€’2w ago
im using 1.2.2
Felix
FelixOPβ€’2w ago
Express Backend "better-auth": "^1.2.6", NextJS Frontend "better-auth": "^1.2.6", "next": "15.3.0", "react": "^19.1.0", Upgraded this morning hoping it would fix something, no change
daanish
daanishβ€’2w ago
no change same problem after upgrade
daanish
daanishβ€’2w ago
this is my prod url you would check it https://roro-ai.com/auth/login
Welcome to client
Generated by create-nx-workspace
Felix
FelixOPβ€’2w ago
Felix on Notion
Session returns null in production | Notion
This scenario works on localhost. However, it doesn't work on the deployment https://auth.playin.gg (Backend: https://api-ts.playin.gg).
bekacru
bekacruβ€’2w ago
looking into it. will report back curios, have you tried not making a call from next server environment and just directly from the client to your auth server?
Felix
FelixOPβ€’2w ago
Mmm, you mean from my localhost to the prod API? The cookie is set on .playin.gg, so that wouldn't work.
daanish
daanishβ€’2w ago
problem is because of useSessionCookie at prod is true
bekacru
bekacruβ€’2w ago
no from your prod client/frontend to your prod api in the notion doc, it looks like your making a call from a server component do you mean useSecureCookies?
daanish
daanishβ€’2w ago
yes
bekacru
bekacruβ€’2w ago
is it working now?
daanish
daanishβ€’2w ago
no i tried in local developement with useSecureCookie to true but i doesn't work
bekacru
bekacruβ€’2w ago
in local host secure cookie doesn't work secure cookies only work under https protocol
daanish
daanishβ€’2w ago
yes, its just clears the cookie after login to redirect.this prod link https://roro-ai.com/auth/login
Welcome to client
Generated by create-nx-workspace
Felix
FelixOPβ€’2w ago
Ahh, I see, so it doesn't work on both the server and client side with getSession/useSession. I have a /protected-client page where the session is also null in prod. I added it in Notion.
bekacru
bekacruβ€’2w ago
in your client code
if (!session.data) {
router.push("/auth/login");
return null;
}
if (!session.data) {
router.push("/auth/login");
return null;
}
session is initally always null so this will always redirect regardless. You need to check if session.isPending is also false
Felix
FelixOPβ€’2w ago
Makes sense, thanks. It actually works client-side now, thanks πŸ™‚ It's strange that I've never encountered this issue with localhost before. I'm still having the same issue server-side, though. One more note: I'm also using the same backend with the Expo plugin. The session works there. It's probably a slightly different architecture, though.
bekacru
bekacruβ€’2w ago
can you check on the server if it returns both error and data null?
daanish
daanishβ€’2w ago
for me problem is solved i added crossSubDomain
Felix
FelixOPβ€’2w ago
HEADERS _ga=GA1.1.1653196102.1744302730; _ga_X5724CWZ7F=GS1.1.1744552980.4.1.1744556733.0.0.0; __Secure-playingg.session_token=xxx SESSION DATA null SESSION ERROR { status: 0, statusText: '' }
Fall
Fallβ€’2w ago
@daanish like this? advanced: { crossSubDomainCookies: { enabled: true, domain: '.osler.app', }, useSecureCookies: true, }
daanish
daanishβ€’2w ago
yes my frontend is roro-ai.com and backend is at backend.roro-ai.com but it created four cookie in browser client
Fall
Fallβ€’2w ago
same, my frontend is osler.app and backend is api.osler.app but the problem continue here
daanish
daanishβ€’2w ago
in prod or localhost
Fall
Fallβ€’2w ago
just prod localhost works fine
daanish
daanishβ€’2w ago
is it https enabled
Fall
Fallβ€’2w ago
yes
daanish
daanishβ€’2w ago
did you try auth.api.getSession()
Felix
FelixOPβ€’2w ago
Unfortunately I don't have access to the internal API because I don't use NextJS as a backend, but only as a frontend. My backend is on express
daanish
daanishβ€’2w ago
ok but you are using async in page.tsx try using async in layout.tsx and pass it as props to child don't use in page.tsx and try it
daanish
daanishβ€’2w ago
NEXTJS_NO_ASYNC_PAGE
Ensures that the exported Next.js page component and its transitive dependencies are not asynchronous, as that blocks the rendering of the page.
daanish
daanishβ€’2w ago
also you are using header as await header() but from nextjs 15 header are async to use header try this fetchOptions: (await header())
Felix
FelixOPβ€’6d ago
Hmm, I see your point about centrally fetching the session async in the layout. However, I also do a lot of fetching on the individual pages server-side, which is why I need async there anyway. Your suggestion is best practice, but it doesn't change my problem that the session is null. It doesn't really matter which call I make from the authclient, whether it's retrieving sessions, accounts, etc. everything that is serverside doesn't work with nextjs 😦 fetchOptions: (await header()) => This doesn't work.
daanish
daanishβ€’6d ago
Do you remove async in page.tsx
rinshad
rinshadβ€’4d ago
I am facing similar issue with oauth , i am using google as social provider , getting getsession is null after login using google ,but session data successfully saved in database , email and password works fine , only problem with google , @bekacru any update on this issue
Felix
FelixOPβ€’3d ago
Unfortunately not. To this day, I'm still facing the same problem: the session can only be accessed client-side. I still haven't had any success with NextJS on the server-side 😦
KiNFiSH
KiNFiSHβ€’3d ago
How are you using it on server side for getting the session
Felix
FelixOPβ€’3d ago
Here is my summarized code

Did you find this page helpful?