diff --git a/drizzle.config.ts b/drizzle.config.ts
new file mode 100644
index 0000000..0b5c1c9
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,11 @@
+import 'dotenv/config';
+import { defineConfig } from 'drizzle-kit';
+
+export default defineConfig({
+ out: './drizzle',
+ schema: './src/db/schema.ts',
+ dialect: 'postgresql',
+ dbCredentials: {
+ url: process.env.DATABASE_URL!,
+ },
+});
diff --git a/drizzle/0000_secret_silver_centurion.sql b/drizzle/0000_secret_silver_centurion.sql
new file mode 100644
index 0000000..5648570
--- /dev/null
+++ b/drizzle/0000_secret_silver_centurion.sql
@@ -0,0 +1,50 @@
+CREATE TABLE "account" (
+ "id" text PRIMARY KEY NOT NULL,
+ "account_id" text NOT NULL,
+ "provider_id" text NOT NULL,
+ "user_id" text NOT NULL,
+ "access_token" text,
+ "refresh_token" text,
+ "id_token" text,
+ "access_token_expires_at" timestamp,
+ "refresh_token_expires_at" timestamp,
+ "scope" text,
+ "password" text,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "session" (
+ "id" text PRIMARY KEY NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "token" text NOT NULL,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ "ip_address" text,
+ "user_agent" text,
+ "user_id" text NOT NULL,
+ CONSTRAINT "session_token_unique" UNIQUE("token")
+);
+--> statement-breakpoint
+CREATE TABLE "user" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "email" text NOT NULL,
+ "email_verified" boolean NOT NULL,
+ "image" text,
+ "created_at" timestamp NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ CONSTRAINT "user_email_unique" UNIQUE("email")
+);
+--> statement-breakpoint
+CREATE TABLE "verification" (
+ "id" text PRIMARY KEY NOT NULL,
+ "identifier" text NOT NULL,
+ "value" text NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "created_at" timestamp,
+ "updated_at" timestamp
+);
+--> statement-breakpoint
+ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
\ No newline at end of file
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..eb0e645
--- /dev/null
+++ b/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,319 @@
+{
+ "id": "46c47915-d6d4-465f-9006-c016fb9d0c1f",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
new file mode 100644
index 0000000..e68f37d
--- /dev/null
+++ b/drizzle/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1740226999095,
+ "tag": "0000_secret_silver_centurion",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index cbe94b5..3da1312 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@ai-sdk/openai": "^1.1.13",
"@hookform/resolvers": "^4.1.0",
+ "@neondatabase/serverless": "^0.10.4",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a084961..d490d15 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@hookform/resolvers':
specifier: ^4.1.0
version: 4.1.0(react-hook-form@7.54.2(react@19.0.0))
+ '@neondatabase/serverless':
+ specifier: ^0.10.4
+ version: 0.10.4
'@radix-ui/react-accordion':
specifier: ^1.2.3
version: 1.2.3(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -82,7 +85,7 @@ importers:
version: 16.4.7
drizzle-orm:
specifier: ^0.39.3
- version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(kysely@0.27.5)(pg@8.13.3)
+ version: 0.39.3(@neondatabase/serverless@0.10.4)(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(kysely@0.27.5)(pg@8.13.3)
lucide-react:
specifier: ^0.475.0
version: 0.475.0(react@19.0.0)
@@ -1104,6 +1107,9 @@ packages:
'@mdx-js/mdx@3.1.0':
resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==}
+ '@neondatabase/serverless@0.10.4':
+ resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==}
+
'@next/env@15.1.7':
resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==}
@@ -1780,6 +1786,9 @@ packages:
'@types/pg@8.11.11':
resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==}
+ '@types/pg@8.11.6':
+ resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==}
+
'@types/react-dom@19.0.3':
resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==}
peerDependencies:
@@ -4287,6 +4296,10 @@ snapshots:
- acorn
- supports-color
+ '@neondatabase/serverless@0.10.4':
+ dependencies:
+ '@types/pg': 8.11.6
+
'@next/env@15.1.7': {}
'@next/swc-darwin-arm64@15.1.7':
@@ -4951,6 +4964,12 @@ snapshots:
pg-protocol: 1.7.1
pg-types: 4.0.2
+ '@types/pg@8.11.6':
+ dependencies:
+ '@types/node': 20.17.19
+ pg-protocol: 1.7.1
+ pg-types: 4.0.2
+
'@types/react-dom@19.0.3(@types/react@19.0.9)':
dependencies:
'@types/react': 19.0.9
@@ -5242,8 +5261,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(kysely@0.27.5)(pg@8.13.3):
+ drizzle-orm@0.39.3(@neondatabase/serverless@0.10.4)(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(kysely@0.27.5)(pg@8.13.3):
optionalDependencies:
+ '@neondatabase/serverless': 0.10.4
'@opentelemetry/api': 1.9.0
'@types/pg': 8.11.11
kysely: 0.27.5
diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts
new file mode 100644
index 0000000..6b047b5
--- /dev/null
+++ b/src/app/api/auth/[...all]/route.ts
@@ -0,0 +1,4 @@
+import { auth } from "@/lib/auth"; // path to your auth file
+import { toNextJsHandler } from "better-auth/next-js";
+
+export const { POST, GET } = toNextJsHandler(auth);
diff --git a/src/components/auth/social-login-button.tsx b/src/components/auth/social-login-button.tsx
index f0c89a8..3c01c52 100644
--- a/src/components/auth/social-login-button.tsx
+++ b/src/components/auth/social-login-button.tsx
@@ -2,12 +2,11 @@
import { Icons } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
-// import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
-// import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
-import { FaBrandsGitHub } from "../icons/github";
-import { FaBrandsGoogle } from "../icons/google";
+import { FaBrandsGitHub } from "@/components/icons/github";
+import { FaBrandsGoogle } from "@/components/icons/google";
+import { authClient } from "@/lib/auth-client";
/**
* social login buttons
@@ -18,12 +17,38 @@ export const SocialLoginButton = () => {
const [isLoading, setIsLoading] = useState<"google" | "github" | null>(null);
const onClick = async (provider: "google" | "github") => {
- setIsLoading(provider);
+ // setIsLoading(provider);
// signIn(provider, {
// callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT,
// });
// no need to reset the loading state, keep loading before webpage redirects
// setIsLoading(null);
+
+ await authClient.signIn.social({
+ /**
+ * The social provider id
+ * @example "github", "google", "apple"
+ */
+ provider: "github",
+ /**
+ * a url to redirect after the user authenticates with the provider
+ * @default "/"
+ */
+ callbackURL: "/dashboard",
+ /**
+ * a url to redirect if an error occurs during the sign in process
+ */
+ errorCallbackURL: "/auth/error",
+ /**
+ * a url to redirect if the user is newly registered
+ */
+ // newUserCallbackURL: "/auth/welcome",
+ /**
+ * disable the automatic redirect to the provider.
+ * @default false
+ */
+ // disableRedirect: true,
+ });
};
return (
@@ -33,7 +58,7 @@ export const SocialLoginButton = () => {
className="w-full"
variant="outline"
onClick={() => onClick("google")}
- // disabled={isLoading === "google"}
+ // disabled={isLoading === "google"}
>
{isLoading === "google" ? (
@@ -47,7 +72,7 @@ export const SocialLoginButton = () => {
className="w-full"
variant="outline"
onClick={() => onClick("github")}
- // disabled={isLoading === "github"}
+ // disabled={isLoading === "github"}
>
{isLoading === "github" ? (
diff --git a/src/db/index.ts b/src/db/index.ts
new file mode 100644
index 0000000..a5137fb
--- /dev/null
+++ b/src/db/index.ts
@@ -0,0 +1,5 @@
+import { drizzle } from 'drizzle-orm/neon-http';
+
+const db = drizzle(process.env.DATABASE_URL!);
+
+export default db;
diff --git a/src/db/schema.ts b/src/db/schema.ts
new file mode 100644
index 0000000..e028842
--- /dev/null
+++ b/src/db/schema.ts
@@ -0,0 +1,47 @@
+import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
+
+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()
+ });
+
+export const session = pgTable("session", {
+ id: text("id").primaryKey(),
+ expiresAt: timestamp('expires_at').notNull(),
+ token: text('token').notNull().unique(),
+ createdAt: timestamp('created_at').notNull(),
+ updatedAt: timestamp('updated_at').notNull(),
+ ipAddress: text('ip_address'),
+ userAgent: text('user_agent'),
+ userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
+ });
+
+export const account = pgTable("account", {
+ id: text("id").primaryKey(),
+ accountId: text('account_id').notNull(),
+ providerId: text('provider_id').notNull(),
+ userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
+ accessToken: text('access_token'),
+ refreshToken: text('refresh_token'),
+ idToken: text('id_token'),
+ accessTokenExpiresAt: timestamp('access_token_expires_at'),
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
+ scope: text('scope'),
+ password: text('password'),
+ createdAt: timestamp('created_at').notNull(),
+ updatedAt: timestamp('updated_at').notNull()
+ });
+
+export const verification = pgTable("verification", {
+ id: text("id").primaryKey(),
+ identifier: text('identifier').notNull(),
+ value: text('value').notNull(),
+ expiresAt: timestamp('expires_at').notNull(),
+ createdAt: timestamp('created_at'),
+ updatedAt: timestamp('updated_at')
+ });
diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts
new file mode 100644
index 0000000..9d614e2
--- /dev/null
+++ b/src/lib/auth-client.ts
@@ -0,0 +1,5 @@
+import { createAuthClient } from "better-auth/react";
+
+export const authClient = createAuthClient({
+ baseURL: process.env.NEXT_PUBLIC_APP_URL!,
+})
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
new file mode 100644
index 0000000..d76a427
--- /dev/null
+++ b/src/lib/auth.ts
@@ -0,0 +1,26 @@
+import { betterAuth } from "better-auth";
+import { drizzleAdapter } from "better-auth/adapters/drizzle";
+import db from "@/db/index";
+import { user, session, account, verification } from "@/db/schema";
+
+export const auth = betterAuth({
+ database: drizzleAdapter(db, {
+ provider: "pg", // or "mysql", "sqlite"
+ schema: {
+ user: user,
+ session: session,
+ account: account,
+ verification: verification,
+ },
+ }),
+ emailAndPassword: {
+ enabled: true
+ },
+ socialProviders: {
+ github: {
+ enabled: true,
+ clientId: process.env.GITHUB_CLIENT_ID!,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
+ }
+ },
+});