Switch from SuperTokens to NextAuth.js

BREAKING CHANGE: Replace SuperTokens with NextAuth.js

Why:
- SuperTokens had persistent Traefik routing issues
- SSL certificate not issuing correctly
- Complex infrastructure (separate container)
- NextAuth runs in Next.js app (simpler, no separate service)

Changes:
- Install next-auth, @auth/prisma-adapter, prisma
- Create NextAuth API route: app/api/auth/[...nextauth]/route.ts
- Add Prisma schema for NextAuth tables (users, sessions, accounts)
- Update auth page to use NextAuth signIn()
- Remove all SuperTokens code and dependencies
- Keep same Google OAuth (just simpler integration)

Benefits:
- No separate auth service needed
- No Traefik routing issues
- Sessions stored in Montreal PostgreSQL
- Simpler configuration
- Battle-tested, widely used

All authentication data stays in Montreal!

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-17 15:12:21 -08:00
parent 8cd95607a4
commit bbb22f1c37
12 changed files with 534 additions and 632 deletions

33
lib/auth/authOptions.ts Normal file
View File

@@ -0,0 +1,33 @@
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
],
pages: {
signIn: "/auth",
error: "/auth",
},
callbacks: {
async session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
session: {
strategy: "database",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
secret: process.env.NEXTAUTH_SECRET,
};

View File

@@ -1,55 +0,0 @@
import ThirdPartyNode from "supertokens-node/recipe/thirdparty";
import EmailPasswordNode from "supertokens-node/recipe/emailpassword";
import SessionNode from "supertokens-node/recipe/session";
import { TypeInput } from "supertokens-node/types";
import { AppInfoUserInput } from "supertokens-node/types";
export const backendConfig = (): TypeInput => {
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://vibnai.com";
const appInfo: AppInfoUserInput = {
appName: "Vib'n",
apiDomain: appUrl,
websiteDomain: appUrl,
apiBasePath: "/api/auth",
websiteBasePath: "/auth",
};
return {
framework: "custom",
supertokens: {
connectionURI: process.env.SUPERTOKENS_CONNECTION_URI || "http://j04ckwg0k040o08gc04gs80o:3567",
apiKey: process.env.SUPERTOKENS_API_KEY || "",
},
appInfo,
recipeList: [
ThirdPartyNode.init({
signInAndUpFeature: {
providers: [
{
config: {
thirdPartyId: "google",
clients: [{
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}],
},
},
{
config: {
thirdPartyId: "github",
clients: [{
clientId: process.env.GITHUB_CLIENT_ID || "",
clientSecret: process.env.GITHUB_CLIENT_SECRET || "",
}],
},
},
],
},
}),
EmailPasswordNode.init(),
SessionNode.init(),
],
isInServerlessEnv: true,
};
};

View File

@@ -1,29 +0,0 @@
import ThirdParty, { Google, Github } from "supertokens-auth-react/recipe/thirdparty";
import EmailPassword from "supertokens-auth-react/recipe/emailpassword";
import Session from "supertokens-auth-react/recipe/session";
export const frontendConfig = () => {
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://app.vibnai.com";
return {
appInfo: {
appName: "Vib'n",
apiDomain: appUrl,
websiteDomain: appUrl,
apiBasePath: "/api/auth",
websiteBasePath: "/auth",
},
recipeList: [
ThirdParty.init({
signInAndUpFeature: {
providers: [
Google.init(),
Github.init(),
],
},
}),
EmailPassword.init(),
Session.init(),
],
};
};