- User registration | email and password
- User login | email and password
- User logout
Protected routes
/app/
Unprotected routes
/login
/register
/
- Create new Nuxt 4 project
- Install Basic modules for the project
- Tailwind css
- SEO Module
- Vue Use
- Drizzle ORM
- Install Drizzle ORM
pnpm add drizzle-orm @libsql/client dotenv
pnpm add -D drizzle-kit tsx
-
Create
.env
-
Create config Drizzle
-
Create schema
- Nuxt Image
- Nuxt font
- Nuxt scripts
- Nuxt content
- Formkit
-
Create the config file for the FormKit
-
Update the nuxt.config file
-
Install the theme for the FormKit styles
npx formkit theme --theme=regenesis
-
Include the FormKit theme config to the tailwindcss configuration
-
There is a issue with Formkit and Nuxt when setting the
autimport:true
in the Formkit config to FormKitSchema have an issue with the data banding.
Optional Modules:
- tailwind-variants
- typographty → Tailwind css
- @unlighthouse/nuxt
npx nuxi@latest module add tailwind seo vueuse image font scripts content formkit
- Install the Better auth
- Connect better auth
- Create username and password example
- Install the Better auth package
pnpm add better-auth
- Generate the env
BETTER_AUTH_SECRET
- Create instance of the auth in
lib/auth.ts
- Connect to Drizzle orm
- Create the tables and migrate
npx @better-auth/cli generate
&npx @better-auth/cli migrate
It seems like the import path for the drizzle config need to be relative and not the auto import from Nuxt ~~
The better auth CLI migrate is not compatible with Drizzle ORM Manual generate and migrate command from drizzle-kit
need to be trigger.
The schema generated from Better auth you can move the content from that schema generated to the Drizzle schema
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
firstName: text('firstName').notNull(),
lastName: text('lastName').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('emailVerified', {
mode: "boolean"
}).notNull(),
image: text('image'),
createdAt: integer('createdAt', {
mode: "timestamp"
}).notNull(),
updatedAt: integer('updatedAt', {
mode: "timestamp"
}).notNull()
});
export const session = sqliteTable("session", {
id: text("id").primaryKey(),
expiresAt: integer('expiresAt', {
mode: "timestamp"
}).notNull(),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: text('userId').notNull().references(() => user.id)
});
export const account = sqliteTable("account", {
id: text("id").primaryKey(),
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: text('userId').notNull().references(() => user.id),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
idToken: text('idToken'),
expiresAt: integer('expiresAt', {
mode: "timestamp"
}),
password: text('password')
});
export const verification = sqliteTable("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expiresAt', {
mode: "timestamp"
}).notNull()
});
Note:
The firstName and lastName are fields that I added manually.
Steps After installation
- Create auth route on the server folder
/api/[...auth].ts
import { auth } from "~~/lib/auth";
export default defineEventHandler(async (event) => {
return auth.handler(toWebRequest(event));
});
- Create a
<root Project Directory>/lib
create the following files:
auth.ts
import { betterAuth, type User } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
//You can import the schema from where the drizzle schema is pointing
import * as schema from "../db/schema";
//You probabaly call this db or any other name to the drizzle connection
import { useDrizzle } from "../server/utils/drizzle";
export const auth = betterAuth({
database: drizzleAdapter(useDrizzle(), {
provider: "sqlite",
schema: {
...schema
}
}),
user: {
//Add the extra fields that you have in the user table I created the fistName and last name sot need to add them here
additionalFields: {
firstName: {
type: "string",
fieldName: "firstName",
returned: true,
input: true,
required: true,
},
lastName: {
type: "string",
fieldName: "lastName",
returned: true,
input: true,
required: true,
}
}
},
emailAndPassword: {
enabled: true,
async sendResetPassword(url, user) {
console.log("Reset password url:", url);
},
},
})
auth-client.ts
import { inferAdditionalFields } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/vue";
import { auth } from "./auth"
export const authClient = createAuthClient({
plugins: [inferAdditionalFields<typeof auth>()],
});
export const {
signIn,
signOut,
signUp,
useSession,
forgetPassword,
resetPassword,
} = authClient;
The useDrizzle is coming from
<root Directory>/server/utils/drizzle.ts
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
//If you have a config file if not this is not necessary
import { tursoConfig } from "../../config/turso.config";
//Point to the schema file from Drizzle
import * as schema from "../../db/schema";
const tursoClient = createClient({
url: tursoConfig.url,
authToken: tursoConfig.authToken
})
//Instance of Drizzle to be use anywhere else
export const useDrizzle = () => {
return drizzle(tursoClient)
}
export const tables = schema;
export const UserInsert = schema.user.$inferInsert;
export type UserRegisterType = Omit<typeof UserInsert, "createdAt" | "updatedAt" | "id" | "emailVerified">;
shadcn-vue-landing-page to Nuxt for the home page
- When a new user registered, send a verification email to the user
- If the user is not verified show a verification screen
await authClient.signIn.emailAndPassword({
email: "email@example.com",
password: "password"
}, {
onError: (ctx) => {
// Handle the error
if(ctx.error.status === 403) {
// Display a verification overlay
}
}
});
- Enforce email verification
export const auth = betterAuth({
emailAndPassword: {
requireEmailVerification: true
}
})