Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions dockerfile → Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,24 @@ RUN npm i -g pnpm
RUN pnpm i --frozen-lockfile

COPY . ./
RUN pnpm prisma generate
RUN pnpm run build


# Deployment container
FROM node:lts-alpine AS deployment
WORKDIR /app
# Copy stuff from build container to ensure we have prisma and everything it needs
COPY --from=builder /app/.output ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/pnpm-workspace.yaml ./
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/drizzle.config.ts ./
COPY --from=builder /app/server/db ./server/db
RUN npm i -g pnpm

# Install Prisma without running any scripts to avoid running nuxt scripts
RUN pnpm i --dev --ignore-scripts --frozen-lockfile
# Run the build scripts needed for prisma to work (for migrations and seeding)
RUN pnpm rebuild esbuild better-sqlite3 @prisma/engines prisma
RUN pnpm prisma generate
# Install only tools needed for migration/seed
RUN pnpm i --frozen-lockfile --dev --ignore-scripts
RUN pnpm rebuild esbuild better-sqlite3
COPY --from=builder /app/entrypoint.sh /entrypoint

# Ensure we can actually run the entrypoint script
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Nuxt Template (Better Auth + Prisma + SQLite)
# Nuxt Template (Better Auth + Drizzle + SQLite)

A modern, production-ready Nuxt 4 template featuring a robust authentication system, ORM integration, and a clean UI foundation.

## Features

- **Nuxt 4**: The latest and greatest from the Nuxt team.
- **Better Auth**: Comprehensive authentication with **Email OTP** support.
- **Prisma**: Type-safe ORM for interacting with the database.
- **Drizzle**: Type-safe ORM for interacting with the database.
- **SQLite**: Lightweight, zero-configuration database, ideal for development and small-to-medium projects.
- **Nuxt UI v3**: Beautiful, accessible, and customizable UI components built with Tailwind CSS.
- **Nodemailer**: Pre-configured for sending verification emails via Gmail.
Expand All @@ -15,7 +15,7 @@ A modern, production-ready Nuxt 4 template featuring a robust authentication sys

- **Framework**: [Nuxt](https://nuxt.com/)
- **Auth**: [Better Auth](https://www.better-auth.com/)
- **ORM**: [Prisma](https://www.prisma.io/)
- **ORM**: [Drizzle](https://orm.drizzle.team/)
- **Database**: [SQLite](https://sqlite.org/)
- **UI Framework**: [Nuxt UI](https://ui3.nuxt.com/)
- **Email**: [Nodemailer](https://nodemailer.com/)
Expand Down Expand Up @@ -60,10 +60,15 @@ Open `.env` and configure the following:
### 4. Database Setup

Initialize your SQLite database and run migrations. You will need to run this command anytime you need to change or create a database.
If there are any migrations that need to be run, the command will ask for a name for the migration. You can simply hit enter, or name your migration.

```bash
pnpm prisma:reset
pnpm db:migrate
```

During development, if there are any database changes since the last migration, you will need to generate a new migration.

```bash
pnpm db:generate
```

### 5. Start the development server
Expand All @@ -72,7 +77,7 @@ pnpm prisma:reset
pnpm dev
```

Your application will be available at `http://localhost:3000`. This command also starts **Prisma Studio** automatically.
Your application will be available at `http://localhost:3000`.

### 6. How to Login

Expand All @@ -81,18 +86,19 @@ Login requires an email address that already exists in the database.
- **Option A: Use the seeded user**
Go to `/auth` and log in with `email@example.com`.
- **Option B: Use your own email**
Update `prisma/seed.ts` with your email, then run `pnpm prisma:reset` to re-seed.
Update `server/db/seed.ts` with your email, then run `pnpm db:seed` to re-seed.

**To get your OTP:**

- Check your configured email inbox.
- **Or**, check the **Prisma Studio** tab in your browser and look in the `Verification` table.
- **Or**, run `pnpm db:studio` to open **Drizzle Studio** and look in the `verification` table.

## Project Structure

- `app/`: Frontend code (pages, components, assets, composables).
- `server/`: Backend code (API routes, authentication logic, database utilities).
- `prisma/`: Database schema, migrations, and seed scripts.
- `server/db/`: Database schema and seed scripts.
- `drizzle/`: Generated migrations.
- `public/`: Static assets.

## GitHub Actions Configuration
Expand Down
185 changes: 99 additions & 86 deletions docs/better_auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ This guide covers Better Auth implementation for email OTP authentication in Nux
### 1. Install Dependencies

```bash
npm install better-auth @prisma/adapter-better-sqlite3 nodemailer
npm install -D prisma @types/nodemailer
npm install better-auth @better-auth/drizzle-adapter better-sqlite3 nodemailer
npm install -D drizzle-kit drizzle-orm @types/better-sqlite3 @types/nodemailer
```

### 2. Environment Variables
Expand All @@ -21,77 +21,91 @@ BETTER_AUTH_URL=http://localhost:3000

### 3. Database Configuration

Update `prisma/schema.prisma` with Better Auth models:
Create `server/db/schema.ts` with Better Auth models using Drizzle:

```prisma
generator client {
provider = "prisma-client"
output = "./generated"
}

datasource db {
provider = "sqlite"
}

model User {
id String @id
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]

@@unique([email])
@@map("user")
}

model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@map("session")
}
```typescript
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'

export const user = sqliteTable('user', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('emailVerified', { mode: 'boolean' }).notNull().default(false),
image: text('image'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
})

model Account {
id String @id
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([provider, providerAccountId])
@@map("account")
}
export const session = sqliteTable('session', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
token: text('token').notNull().unique(),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
}, (table) => [
index('session_userId_idx').on(table.userId),
])

export const account = sqliteTable('account', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
idToken: text('idToken'),
accessTokenExpiresAt: integer('accessTokenExpiresAt', { mode: 'timestamp' }),
refreshTokenExpiresAt: integer('refreshTokenExpiresAt', { mode: 'timestamp' }),
scope: text('scope'),
password: text('password'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}, (table) => [
index('account_userId_idx').on(table.userId),
])

export const verification = sqliteTable('verification', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}, (table) => [
index('verification_identifier_idx').on(table.identifier),
])

export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}))

export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, { fields: [session.userId], references: [user.id] }),
}))

export const accountRelations = relations(account, ({ one }) => ({
user: one(user, { fields: [account.userId], references: [user.id] }),
}))
```

model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Configure Drizzle Kit in `drizzle.config.ts`:

@@unique([identifier, value])
@@map("verification")
}
```typescript
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
schema: './server/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
})
```

### 4. Server Auth Configuration
Expand All @@ -100,11 +114,11 @@ Create `server/utils/auth.ts`:

```typescript
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from './prisma'
import { drizzleAdapter } from '@better-auth/drizzle-adapter'
import { db } from './db'

export const auth = betterAuth({
database: prismaAdapter(prisma, {
database: drizzleAdapter(db, {
provider: 'sqlite',
}),
})
Expand Down Expand Up @@ -138,8 +152,8 @@ Update `server/utils/auth.ts`:

```typescript
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from './prisma'
import { drizzleAdapter } from '@better-auth/drizzle-adapter'
import { db } from './db'
import { emailOTP } from 'better-auth/plugins/email-otp'
import nodemailer from 'nodemailer'

Expand All @@ -152,7 +166,7 @@ const transporter = nodemailer.createTransport({
})

export const auth = betterAuth({
database: prismaAdapter(prisma, {
database: drizzleAdapter(db, {
provider: 'sqlite',
}),
plugins: [
Expand Down Expand Up @@ -385,8 +399,8 @@ export default defineEventHandler(async (event) => {
}

// Use authenticated user ID for user-specific operations
const userProfile = await prisma.user.findUnique({
where: { id: session.user.id },
const userProfile = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, session.user.id),
})

return userProfile
Expand Down Expand Up @@ -454,14 +468,14 @@ export default defineEventHandler(async (event) => {

if (session) {
// Authenticated user gets full profile
return await prisma.user.findUnique({
where: { id: session.user.id },
include: { sessions: true, accounts: true },
return await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, session.user.id),
with: { sessions: true, accounts: true },
})
} else {
// Public user gets limited data
return await prisma.user.findMany({
select: {
return await db.query.user.findMany({
columns: {
id: true,
name: true,
email: true,
Expand Down Expand Up @@ -492,14 +506,13 @@ export default defineEventHandler(async (event) => {
}

// Full profile for own user
return await prisma.user.findUnique({
where: { id: userId },
include: {
return await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, userId),
with: {
sessions: true,
accounts: true,
// Add other sensitive fields
},
})
})
```

Loading
Loading