Skip to content

nrjdalal/nrjdalal.com

Repository files navigation

An open source Next.js starter with step-by-step instructions if required.

Follow Neeraj on Twitter

Features · Step by Step · Roadmap · Author · Credits

Onset is a Next.js starter that comes with step-by-step instructions to understand how everything works, easy for both beginners and experts alike and giving you the confidence to customize it to your needs. Built with Next.js 14, Drizzle (Postgres), NextAuth/Auth.js.

Features

Frameworks

  • Next.js – React framework for building performant apps with the best developer experience
  • Auth.js – Handle user authentication with ease with providers like Google, Twitter, GitHub, etc.
  • Drizzle – Typescript-first ORM for Node.js

Platforms

  • Vercel – Easily preview & deploy changes with git
  • Neon – The fully managed serverless Postgres with a generous free tier

Automatic Setup

Installation

Clone & create this repo locally with the following command:

Note: You can use npx or pnpx as well

bunx create-next-app onset-starter --example "https://github.com/nrjdalal/onset"
  1. Install dependencies using pnpm:
bun install
  1. Copy .env.example to .env.local and update the variables.
cp .env.example .env.local
  1. Run the database migrations:
bun db:push
  1. Start the development server:
bun dev

Step by Step

Hint: Using bun instead of npm/pnpm and bunx instead of npx/pnpx. You can use the latter if you want.

Phase 1 (Initialization)

1. Initialize the project

Refs:

bunx create-next-app . --ts --eslint --tailwind --src-dir --app --import-alias "@/*"

2. Install prettier and supporting plugins

Refs:

bun add -D @ianvs/prettier-plugin-sort-imports prettier prettier-plugin-tailwindcss

3. Create prettier.config.js

/** @type {import('prettier').Config} */
module.exports = {
  semi: false,
  singleQuote: true,
  plugins: [
    '@ianvs/prettier-plugin-sort-imports',
    'prettier-plugin-tailwindcss',
  ],
}

4. Create src/lib/fonts.ts

Refs:

import {
  JetBrains_Mono as FontMono,
  DM_Sans as FontSans,
} from 'next/font/google'

export const fontMono = FontMono({
  subsets: ['latin'],
  variable: '--font-mono',
})

export const fontSans = FontSans({
  subsets: ['latin'],
  variable: '--font-sans',
})

5. Install clsx and tailwind-merge

Refs:

bun add clsx tailwind-merge

6. Create src/lib/utils.ts

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export const cn = (...inputs: ClassValue[]) => {
  return twMerge(clsx(inputs))
}

7. Update src/app/layout.tsx

import './globals.css'
import { fontMono, fontSans } from '@/lib/fonts'
import { cn } from '@/lib/utils'
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Onset',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body
        className={cn(
          'min-h-[100dvh] font-sans antialiased',
          fontMono.variable,
          fontSans.variable,
        )}
      >
        {children}
      </body>
    </html>
  )
}

Phase 2 (Database)

1. Install drizzle and supporting packages

Refs:

bun add @auth/drizzle-adapter drizzle-orm pg postgres
bun add -D drizzle-kit

2. Create src/lib/db/schema.ts

Refs:

import type { AdapterAccount } from '@auth/core/adapters'
import {
  integer,
  pgTable,
  primaryKey,
  text,
  timestamp,
} from 'drizzle-orm/pg-core'

export const users = pgTable('user', {
  id: text('id').notNull().primaryKey(),
  name: text('name'),
  email: text('email').notNull(),
  emailVerified: timestamp('emailVerified', { mode: 'date' }),
  image: text('image'),
})

export const accounts = pgTable(
  'account',
  {
    userId: text('userId')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    type: text('type').$type<AdapterAccount['type']>().notNull(),
    provider: text('provider').notNull(),
    providerAccountId: text('providerAccountId').notNull(),
    refresh_token: text('refresh_token'),
    access_token: text('access_token'),
    expires_at: integer('expires_at'),
    token_type: text('token_type'),
    scope: text('scope'),
    id_token: text('id_token'),
    session_state: text('session_state'),
  },
  (account) => ({
    compoundKey: primaryKey(account.provider, account.providerAccountId),
  }),
)

export const sessions = pgTable('session', {
  sessionToken: text('sessionToken').notNull().primaryKey(),
  userId: text('userId')
    .notNull()
    .references(() => users.id, { onDelete: 'cascade' }),
  expires: timestamp('expires', { mode: 'date' }).notNull(),
})

export const verificationTokens = pgTable(
  'verificationToken',
  {
    identifier: text('identifier').notNull(),
    token: text('token').notNull(),
    expires: timestamp('expires', { mode: 'date' }).notNull(),
  },
  (vt) => ({
    compoundKey: primaryKey(vt.identifier, vt.token),
  }),
)

3. Create src/lib/db/index.ts

Refs:

import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'

const queryClient = postgres(process.env.POSTGRES_URL as string)

export const db: PostgresJsDatabase = drizzle(queryClient)

4. Create drizzle.config.ts

import type { Config } from 'drizzle-kit'

export default {
  schema: './src/lib/db/schema.ts',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.POSTGRES_URL as string,
  },
} satisfies Config

5. Copy .env.example as .env.local

Hint: You can use pglaunch to generate a postgres url

POSTGRES_URL="**********"

6. Update package.json

{
  // ...
  "scripts": {
    // ...
    "db:push": "drizzle-kit push:pg --config drizzle.config.ts",
    "db:studio": "drizzle-kit studio --config drizzle.config.ts"
  }
  // ...
}

7. Update tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext"
    // ...
  }
  // ...
}
7. Run db:push to create tables
bun db:push

Phase 3 (Authentication)

1. Install next-auth

bun add next-auth

2. Update .env.local

# ...
NEXTAUTH_SECRET="**********"

GOOGLE_CLIENT_ID="**********"
GOOGLE_CLIENT_SECRET="**********"
  1. Create src/lib/auth.ts
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { eq } from 'drizzle-orm'
import { NextAuthOptions } from 'next-auth'
import type { User } from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'

export const authOptions: NextAuthOptions = {
  adapter: DrizzleAdapter(db),
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    }),
  ],
  pages: {
    signIn: '/access',
  },
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    async session({ token, session }) {
      if (token) {
        session.user.id = token.id
        session.user.name = token.name
        session.user.email = token.email
        session.user.image = token.picture
      }

      return session
    },
    async jwt({ token, user }) {
      const [dbUser] = await db
        .select()
        .from(users)
        .where(eq(users.email, token.email || ''))
        .limit(1)

      if (!dbUser) {
        if (user) {
          token.id = user?.id
        }
        return token
      }

      return {
        id: dbUser.id,
        name: dbUser.name,
        email: dbUser.email,
        picture: dbUser.image,
      }
    },
  },
}

declare module 'next-auth/jwt' {
  interface JWT {
    id: string
  }
}

declare module 'next-auth' {
  interface Session {
    user: User & {
      id: string
    }
  }
}

3. Create src/app/api/auth/[...nextauth]/route.ts

import { authOptions } from '@/lib/auth'
import NextAuth from 'next-auth'

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

4. Create src/middleware.ts

import { getToken } from 'next-auth/jwt'
import { withAuth } from 'next-auth/middleware'
import { NextResponse } from 'next/server'

export default withAuth(
  async function middleware(req) {
    const token = await getToken({ req })

    const isAuth = !!token
    const isAuthPage = req.nextUrl.pathname.startsWith('/access')

    if (isAuthPage) {
      if (isAuth) {
        return NextResponse.redirect(new URL('/dashboard', req.url))
      }

      return null
    }

    if (!isAuth) {
      let from = req.nextUrl.pathname
      if (req.nextUrl.search) {
        from += req.nextUrl.search
      }

      return NextResponse.redirect(
        new URL(`/access?from=${encodeURIComponent(from)}`, req.url),
      )
    }
  },
  {
    callbacks: {
      async authorized() {
        return true
      },
    },
  },
)

export const config = {
  matcher: ['/access', '/dashboard/:path*'],
}

5. Create src/app/(auth)/access/page.tsx

'use client'

import { signIn } from 'next-auth/react'
import Link from 'next/link'

const Page = () => {
  return (
    <div className="flex min-h-[100dvh] items-center justify-center gap-4">
      <Link
        href={'/'}
        className="rounded-md bg-slate-900 px-8 py-2.5 text-white"
      >
        Home
      </Link>
      <button
        className="rounded-md border px-8 py-2.5"
        onClick={() => signIn('google')}
      >
        Continue with Google
      </button>
    </div>
  )
}

export default Page

6. Create src/app/(admin)/dashboard/page.tsx

'use client'

import { signOut } from 'next-auth/react'
import Link from 'next/link'

const Page = () => {
  return (
    <div className="flex min-h-[100dvh] items-center justify-center gap-4">
      <Link
        href={'/'}
        className="rounded-md bg-slate-900 px-8 py-2.5 text-white"
      >
        Home
      </Link>
      <button
        className="rounded-md border px-8 py-2.5"
        onClick={() => signOut()}
      >
        Sign Out
      </button>
    </div>
  )
}

export default Page

Roadmap

  • Light and dark mode
  • To added fine-grained instructions
  • More features and points to be added

Author

Created by @nrjdalal in 2023, released under the MIT license.

Credits

This project is inspired by @shadcn's Taxonomy.

About

My personal portfolio. Clone it to make yours.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published