This scenario works on localhost. However, it doesn't work on the deployment https://auth.playin.gg (Backend: https://api-ts.playin.gg).
Authentication works. The cookie is set correctly.
You are redirected to /protected after signing in.
See logs, but the session is null, which is why you are immediately redirected back to /auth/login.
Express Backend (index.ts)
import v2Routes from "@/routes/v2.routes";
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;
app.use(
cors({
origin: ["<https://auth.playin.gg>", "<http://localhost:3000>"],
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
})
);
app.all("/api/auth/*", toNodeHandler(auth));
// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// @desc Root route
// @route GET /
// @access Public
app.get("/", (req: Request, res: Response) => {
res.send({
message: "Welcome to the PlayinGG API!",
version: "v2.0.0",
environment: process.env.NODE_ENV,
status: 200,
});
});
// Routes (v2)
app.use("/v2", v2Routes);
app.listen(port, () => {
console.log(`🟢 [PlayinGG API]: Running at ${baseUrl}:${port}`);
});
Express Backend (auth.ts)
import { expo } from "@better-auth/expo";
import { betterAuth } from "better-auth";
import { admin, emailOTP, twoFactor } from "better-auth/plugins";
import { passkey } from "better-auth/plugins/passkey";
import dotenv from "dotenv";
import { createPool } from "mysql2/promise";
dotenv.config({ path: "./.env" });
export const auth = betterAuth({
appName: "PlayinGG",
advanced: {
cookiePrefix: "playingg",
crossSubDomainCookies: {
enabled: true,
domain: ".playin.gg",
},
useSecureCookies: true,
},
emailAndPassword: {
requireEmailVerification: true,
enabled: true,
sendResetPassword: async ({ user, url, token }, request) => {
console.log(user, url, token);
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url, token }, request) => {
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: ["myapp://", "<http://localhost:3000>", "<https://www.playin.gg>", "<https://playin.gg>", "<https://portal.playin.gg>", "<https://auth.playin.gg>"],
plugins: [
passkey(),
twoFactor(),
expo(),
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
// Implement the sendVerificationOTP method to send the OTP to the user's email address
},
}),
admin(),
],
});
BETTER_AUTH_URL=https://api-ts.playin.gg
NextJS Frontend (auth.ts)
import { emailOTPClient, 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(), emailOTPClient()],
});
NextJS Frontend (/app/protected/page.tsx)
import LogoutButton from "@/components/auth/logout-button";
import AccountList from "@/components/settings/account-list";
import MFASection from "@/components/settings/mfa-section";
import PasskeyList from "@/components/settings/passkey-list";
import SessionList from "@/components/settings/session-list";
import Test from "@/components/test";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
const ProtectedSSRPage = async () => {
const heads = await headers();
console.log("HEADERS", heads.get("cookie"));
const { data: session } = await auth.getSession({
fetchOptions: {
headers: await headers(),
},
});
console.log("SESSION", session);
if (!session) {
return redirect("/auth/login");
}
return (
<div className="w-full max-w-5xl mx-auto my-10">
<h1 className="font-bold text-white text-2xl">Welcome {session.user.name}</h1>
<p className="">
Your email: <span className="">{session.user.email}</span>
</p>
<p className="">
Your ID: <span className="">{session.user.id}</span>
</p>
<div className="mt-6">
<LogoutButton />
</div>
<div className="mt-6">
<SessionList />
</div>
<div className="mt-6">
<PasskeyList />
</div>
<div className="mt-6">
<AccountList />
</div>
<MFASection twoFactorEnabled={session.user.twoFactorEnabled ?? false} />
<Test />
</div>
);
};
export default ProtectedSSRPage;
LOG OUTPUT
HEADERS _ga=GA1.1.1653196102.1744302730; _ga_X5724CWZ7F=GS1.1.1744552980.4.1.1744556733.0.0.0; __Secure-playingg.session_token=JhBmnxgfHDqPEbwr5WteRHzaoTBBJnpq.PK8HarkKOkhHG%2Fsk45x2qj9Y0XBUEkmehVSP21rl9WQ%3D
SESSION null
NextJS Frontend (/app/protected-client/page.tsx)