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)