diff --git a/.cspell.json b/.cspell.json index afa3a20..3029eae 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,7 @@ "typescript" ], "files": ["**", ".vscode/**", ".github/**"], - "ignorePaths": ["bun.lock", "drizzle/**"], + "ignorePaths": ["bun.lock"], "ignoreRegExpList": ["apiKey='[a-zA-Z0-9-]{32}'"], "import": [ "@cspell/dict-bash/cspell-ext.json", @@ -43,7 +43,6 @@ "nums", "dall", "daveyplate", - "neondatabase", "shadcn", "requiredness", "bradlc", diff --git a/.env.example b/.env.example index 5b97a17..9cd94bd 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,9 @@ -# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo. -# Keep this file up-to-date when you add new variables to \`.env\`. +# Environment variables are optional for this starter. +# You can copy this file to `.env` and uncomment values if needed. -# This file will be committed to version control, so make sure not to have any secrets in it. -# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. +# Public base URL of your site (optional) +# NEXT_PUBLIC_BASE_URL=http://localhost:3000 -# When adding additional environment variables, the schema in "/src/env.ts" -# should be updated accordingly. - -# Database URL -# The database URL is used to connect to your database. It's used for commenting, and authentication. -DATABASE_URL="postgresql://postgres:@localhost:5432/starter" - -# Authentication -# You can generate the secret via 'openssl rand -base64 32' on Unix -# @see https://www.better-auth.com/docs/installation -BETTER_AUTH_SECRET="" - -# CORS allowed origins for auth endpoints -# Accepts a comma-separated list OR a JSON array of URLs -CORS_ORIGIN="http://localhost:3000" - -# Production URL -# Used for authentication and metadata generation -NEXT_PUBLIC_BASE_URL=http://localhost:3000 +# Build-time / tooling (optional) +# ANALYZE=false +# SOURCE_MAPS=false diff --git a/README.md b/README.md index 1b2b7a0..39cf33c 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,34 @@ -

- -

+# Next.js Starter -

- Better Auth Next.js Starter -

+A minimal, clean Next.js starter that preserves a polished UI foundation without authentication or a database. -

- - - - - - - -

+Features +- Next.js (App Router) + TypeScript (strict) +- Tailwind CSS v4 with modern design tokens +- shadcn/ui components (Button, Card, Input, and more) +- Dark mode via next-themes +- Toasts with sonner +- Navigation progress bar with bprogress +- SEO metadata helpers and sensible security headers -## Quickstart +Pages included +- `/` — Welcome page with quick links and stack overview +- `/about` — Example static page showing metadata usage +- `/components` — Simple gallery using shadcn/ui components +- `/api/health` — Minimal API responding with `{ status: "ok" }` -1) Create a PostgreSQL database (local or hosted). +Getting started +This starter has zero required environment variables. Clone the repo and start building. -2) Copy `.env.example` in the project root and fill in the variables below. - -3) Generate schema and run database migrations. - -```bash -bun run auth:generate -bun run db:generate -bun run db:push -``` - -4) Start the dev server. - -```bash -bun run dev -``` - - -## Environment variables - -All configuration is typed and validated in `src/env.ts`. Requiredness differs between development and production as described. - -```bash -# Runtime (server-only) - -# PostgreSQL connection string (required) -# Format must be a valid URL -DATABASE_URL="postgresql://postgres:@localhost:5432/starter" - -# Better Auth secret (required in production; optional in development) -# You can generate via: openssl rand -base64 32 -# Docs: https://www.better-auth.com/docs/installation#set-environment-variables -BETTER_AUTH_SECRET="" - -# CORS allowed origins for auth endpoints -# Accepts a comma-separated list OR a JSON array of URLs -# Examples: -# CORS_ORIGIN="http://localhost:3000,https://your-domain.com" -# CORS_ORIGIN='["http://localhost:3000","https://your-domain.com"]' -CORS_ORIGIN="http://localhost:3000" - -# Google Gemini API key (required) -GOOGLE_GENERATIVE_AI_API_KEY=your_gemini_api_key_here - - -# Runtime (client-exposed) - -# Public base URL of your site (required) -# Must be a full URL including protocol -# Example (dev): http://localhost:3000 -# Example (prod): https://your-domain.com -NEXT_PUBLIC_BASE_URL=http://localhost:3000 - - -# Build-time / tooling (optional) - -# Enable bundle analyzer for production builds -ANALYZE=false - -# Emit production browser source maps -SOURCE_MAPS=false -``` +How to add Auth and DB later +- Auth: better-auth — https://better-auth.com/docs +- UI: better-auth-ui — https://better-auth-ui.com +- ORM/DB: Drizzle ORM — https://orm.drizzle.team/docs Notes -- `CORS_ORIGIN` may be a single URL, a comma-separated list, or a JSON array. All values must be valid URLs. -- `BETTER_AUTH_SECRET` is mandatory in production. In development it can be left empty to use the library defaults, but setting it early avoids surprises. -- `NEXT_PUBLIC_BASE_URL` is used for authentication, metadata, and sitemap generation. It must be the publicly reachable URL of your app. - - -## Database and migrations - -This starter uses Drizzle ORM with PostgreSQL. Common commands: - -```bash -# Generate SQL from schema -bun run db:generate - -# Apply migrations -bun run db:push - -# Open Drizzle Studio -bun run db:studio -``` - - -## Features - -[Better Auth](https://better-auth.com) - -[Better Auth UI](https://better-auth-ui.com) - -[shadcn/ui](https://ui.shadcn.com) - -[TailwindCSS](https://tailwindcss.com) - -[Drizzle ORM](https://orm.drizzle.team) - -[PostgreSQL](https://postgresql.org) - -[Biome](https://biomejs.dev) - -[Next.js](https://nextjs.org) - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme). +- The project intentionally excludes auth and database code to give you a clean base. Add your preferred solutions when you need them. +- Type checking, linting, and formatting are configured via TypeScript and Biome. +- Sitemap generation is included via next-sitemap. -After creating a project, set the environment variables from the section above in the Vercel project settings and redeploy. \ No newline at end of file +License +MIT diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index a7bf83a..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from 'drizzle-kit' - -import { env } from '@/env' - -export default { - schema: './src/server/db/schema/index.ts', - dialect: 'postgresql', - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ['starter_*'], -} satisfies Config diff --git a/drizzle/0000_powerful_ulik.sql b/drizzle/0000_powerful_ulik.sql deleted file mode 100644 index 34610cb..0000000 --- a/drizzle/0000_powerful_ulik.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE TABLE "accounts" ( - "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 "sessions" ( - "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 "sessions_token_unique" UNIQUE("token") -); ---> statement-breakpoint -CREATE TABLE "users" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "email" text NOT NULL, - "email_verified" boolean DEFAULT false NOT NULL, - "image" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - "is_anonymous" boolean, - CONSTRAINT "users_email_unique" UNIQUE("email") -); ---> statement-breakpoint -CREATE TABLE "verifications" ( - "id" text PRIMARY KEY NOT NULL, - "identifier" text NOT NULL, - "value" text NOT NULL, - "expires_at" timestamp NOT NULL, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL -); ---> statement-breakpoint -ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("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 deleted file mode 100644 index 1c98601..0000000 --- a/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,314 +0,0 @@ -{ - "id": "cd8f4897-b6f0-4396-aa25-7cd6e53d1c9a", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.accounts": { - "name": "accounts", - "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": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sessions": { - "name": "sessions", - "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": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sessions_token_unique": { - "name": "sessions_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "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, - "default": false - }, - "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 - }, - "is_anonymous": { - "name": "is_anonymous", - "type": "boolean", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verifications": { - "name": "verifications", - "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": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json deleted file mode 100644 index 741673f..0000000 --- a/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1757625148079, - "tag": "0000_powerful_ulik", - "breakpoints": true - } - ] -} diff --git a/package.json b/package.json index f2b478c..291b662 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "better-auth-nextjs-starter", + "name": "nextjs-starter", "version": "1.0.0", "private": true, "type": "module", @@ -18,15 +18,7 @@ "check:unsafe": "biome check --write --unsafe .", "check:write": "biome check --write .", "typecheck": "tsc --noEmit --incremental false", - "check:spelling": "cspell -c .cspell.json --no-progress --no-summary --no-must-find-files --unique", - "auth:generate": "bunx @better-auth/cli generate --config src/server/auth/index.ts --output src/server/db/schema/auth.ts --yes && bun format src/server/db/schema/auth.ts", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio", - "db:push": "drizzle-kit push", - "db:pull": "drizzle-kit pull", - "db:check": "drizzle-kit check", - "db:up": "drizzle-kit up" + "check:spelling": "cspell -c .cspell.json --no-progress --no-summary --no-must-find-files --unique" }, "dependencies": { "@bprogress/core": "^1.3.4", @@ -34,7 +26,6 @@ "@cspell/dict-bash": "^4.2.1", "@cspell/dict-npm": "^5.2.17", "@icons-pack/react-simple-icons": "^13.7.0", - "@neondatabase/serverless": "^1.0.1", "@next/bundle-analyzer": "^15.5.3", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.12", @@ -47,13 +38,10 @@ "@radix-ui/react-use-controllable-state": "^1.2.2", "@t3-oss/env-core": "^0.13.8", "@t3-oss/env-nextjs": "^0.13.8", - "better-auth": "^1.3.9", - "better-auth-ui": "npm:@daveyplate/better-auth-ui", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cspell": "^9.2.1", "dotenv": "^17.2.2", - "drizzle-orm": "^0.44.5", "embla-carousel-react": "^8.6.0", "harden-react-markdown": "^1.0.5", "jiti": "^1.21.7", @@ -76,7 +64,6 @@ "zod": "^4.1.8" }, "devDependencies": { - "@better-auth/cli": "^1.3.9", "@biomejs/biome": "2.2.0", "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", @@ -86,7 +73,6 @@ "@types/react-dom": "^19.1.9", "@types/react-syntax-highlighter": "^15.5.13", "cross-env": "^10.0.0", - "drizzle-kit": "^0.31.4", "lefthook": "^1.13.0", "tailwindcss": "^4.1.13", "tw-animate-css": "^1.3.8", diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..4525d59 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "About", + description: "About this Next.js Starter", +} + +export default function AboutPage() { + return ( +
+

About

+

+ This is a minimal Next.js starter focused on a clean foundation: TypeScript, App Router, Tailwind CSS v4, and shadcn/ui. It includes dark mode, toasts, and a page-load progress bar. +

+

+ It intentionally excludes authentication and database setup so you can integrate your preferred solutions later. +

+
+ ) +} diff --git a/src/app/account/[path]/page.tsx b/src/app/account/[path]/page.tsx deleted file mode 100644 index 2c56187..0000000 --- a/src/app/account/[path]/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { AccountView } from 'better-auth-ui' -import { accountViewPaths } from 'better-auth-ui/server' - -export const dynamicParams = false - -export function generateStaticParams() { - return Object.values(accountViewPaths).map((path) => ({ - path, - })) -} - -export default async function AccountPage({ - params, -}: PageProps<'/auth/[path]'>) { - const { path } = await params - - return ( -
- -
- ) -} diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts deleted file mode 100644 index 191f4bb..0000000 --- a/src/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { toNextJsHandler } from 'better-auth/next-js' -import { auth } from '@/server/auth' - -export const { POST, GET } = toNextJsHandler(auth) diff --git a/src/app/api/auth/guest/route.ts b/src/app/api/auth/guest/route.ts deleted file mode 100644 index 605e3f0..0000000 --- a/src/app/api/auth/guest/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextResponse } from 'next/server' -// import { auth, getSession } from '@/server/auth' - -// export async function GET(request: NextRequest) { -// const { searchParams } = new URL(request.url) -// const redirectUrl = searchParams.get('redirectUrl') || '/' - -// const session = await getSession(request) - -// if (session?.session?.token) { -// return NextResponse.redirect(new URL('/', request.url)) -// } - -// const signInResponse = await auth.api.signInAnonymous({ -// query: { -// callbackURL: redirectUrl, -// }, -// asResponse: true, -// headers: request.headers, -// }) - -// const response = NextResponse.redirect(new URL(redirectUrl, request.url)) - -// const setCookie = signInResponse.headers.get('set-cookie') -// if (setCookie) { -// response.headers.set('set-cookie', setCookie) -// } - -// return response -// } - -export async function GET() { - return NextResponse.json({ message: 'This feature has been disabled.' }) -} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..c681db4 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server" + +export async function GET() { + return NextResponse.json({ status: "ok" }) +} diff --git a/src/app/auth/[path]/page.tsx b/src/app/auth/[path]/page.tsx deleted file mode 100644 index 4a284c7..0000000 --- a/src/app/auth/[path]/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { AuthView } from 'better-auth-ui' -import { authViewPaths } from 'better-auth-ui/server' - -export const dynamicParams = false - -export function generateStaticParams() { - return Object.values(authViewPaths).map((path) => ({ path })) -} - -export default async function AuthPage({ params }: PageProps<'/auth/[path]'>) { - const { path } = await params - - return ( -
- -
- ) -} diff --git a/src/app/components/page.tsx b/src/app/components/page.tsx new file mode 100644 index 0000000..048cd04 --- /dev/null +++ b/src/app/components/page.tsx @@ -0,0 +1,38 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" + +export default function ComponentsPage() { + return ( +
+

Components

+

A few examples from shadcn/ui.

+ +
+ + + Button + Variants you can use + + + + + + + + + + + + + Input + Simple text input + + + + + +
+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 998640e..45888b5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,34 +1,47 @@ -'use client' - -import { UserAvatar } from 'better-auth-ui' -import { Loader2 } from 'lucide-react' -import { redirect } from 'next/navigation' -import { useSession } from '@/lib/auth-client' +import Link from "next/link" +import { ArrowRight } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" export default function Home() { - const { data: session, isPending: isLoading } = useSession() - const user = session?.user - - if (!isLoading && !user) { - return redirect('/auth/sign-in?redirectTo=/') - } - return ( -
- {isLoading && ( -
- +
+
+

Welcome to Next.js Starter

+

+ A minimal, clean Next.js starter with TypeScript, Tailwind CSS, shadcn/ui, dark mode, toasts, and a page-load progress bar. +

+
+ + +
- )} +
- {!isLoading && user && ( -
- -

- {`Welcome, ${user.name ?? 'there'}!`} -

-
- )} -
+
+ + + Tech stack + + + Next.js (App Router), TypeScript, Tailwind CSS v4, shadcn/ui, next-themes, sonner, bprogress + + + + + Ready to extend + + + Add your own routes, APIs, and components. Auth and database are intentionally omitted for a clean start. + + +
+ ) } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 92c76b4..76137f6 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,18 +1,12 @@ -'use client' +"use client" -import { ProgressProvider } from '@bprogress/next/app' -import { AuthUIProvider } from 'better-auth-ui' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { ThemeProvider } from 'next-themes' -import type { ReactNode } from 'react' -import { Toaster } from 'sonner' -import { TailwindIndicator } from '@/components/tailwind-indicator' -import { authClient } from '@/lib/auth-client' +import { ProgressProvider } from "@bprogress/next/app" +import { ThemeProvider } from "next-themes" +import type { ReactNode } from "react" +import { Toaster } from "sonner" +import { TailwindIndicator } from "@/components/tailwind-indicator" export function Providers({ children }: { children: ReactNode }) { - const router = useRouter() - return ( - { - router.refresh() + - - {children} - - - - + {children} + + + ) } diff --git a/src/components/header.tsx b/src/components/header.tsx index 3406f94..6b0f0ba 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,34 +1,30 @@ -import { GitHubIcon, UserButton } from 'better-auth-ui' -import Link from 'next/link' -import { APP_NAME, logo } from '@/lib/constants' -import { ModeToggle } from './mode-toggle' -import { Button } from './ui/button' +import Link from "next/link" +import { Github } from "lucide-react" +import { APP_NAME, logo } from "@/lib/constants" +import { ModeToggle } from "./mode-toggle" +import { Button } from "./ui/button" export function Header() { return ( -
+
{logo} {APP_NAME} + +
- - -
) diff --git a/src/components/logos/mark.tsx b/src/components/logos/mark.tsx new file mode 100644 index 0000000..6aed959 --- /dev/null +++ b/src/components/logos/mark.tsx @@ -0,0 +1,24 @@ +import type { SVGProps } from 'react' + +const BetterAuth = (props: SVGProps) => ( + + + + + +) + +export default BetterAuth diff --git a/src/env.ts b/src/env.ts index 24b8bbe..dcf7349 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,41 +1,14 @@ -import { vercel } from '@t3-oss/env-core/presets-zod' -import { createEnv } from '@t3-oss/env-nextjs' -import { z } from 'zod' +import { vercel } from "@t3-oss/env-core/presets-zod" +import { createEnv } from "@t3-oss/env-nextjs" +import { z } from "zod" export const env = createEnv({ extends: [vercel()], server: { - NODE_ENV: z - .enum(['development', 'production', 'test']) - .default('development'), - // Database - DATABASE_URL: z.url(), - // Auth - BETTER_AUTH_SECRET: - process.env.NODE_ENV === 'production' - ? z.string().min(1) - : z.string().min(1).optional(), - CORS_ORIGIN: z.preprocess((val) => { - if (typeof val === 'string') { - const s = val.trim() - if (s === '') return undefined - try { - const parsed = JSON.parse(s) - if (Array.isArray(parsed)) return parsed - } catch { - // ignore JSON parse errors - } - return s - .split(',') - .map((p) => p.trim()) - .filter(Boolean) - } - return val - }, z.array(z.url()).optional()), - // BETTER_AUTH_URL: z.string().min(1).optional(), + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), }, client: { - NEXT_PUBLIC_BASE_URL: z.url(), + NEXT_PUBLIC_BASE_URL: z.string().url().optional(), }, experimental__runtimeEnv: { NEXT_PUBLIC_BASE_URL: diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts deleted file mode 100644 index 0ec1515..0000000 --- a/src/lib/auth-client.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - anonymousClient, - inferAdditionalFields, -} from 'better-auth/client/plugins' -import { createAuthClient } from 'better-auth/react' -import { toast } from 'sonner' - -import type { auth } from '@/server/auth' -// import { env } from '@/env' - -// @see https://github.com/better-auth/better-auth/issues/1391 -export const authClient: ReturnType = createAuthClient( - { - plugins: [inferAdditionalFields(), anonymousClient()], - // baseURL: env.NEXT_PUBLIC_BASE_URL, - fetchOptions: { - onError(e) { - if (e.error.status === 429) { - toast.error('Too many requests. Please try again later.') - } - }, - }, - } -) - -export const signIn: typeof authClient.signIn = authClient.signIn -export const signOut: typeof authClient.signOut = authClient.signOut -export const useSession: typeof authClient.useSession = authClient.useSession - -export type User = (typeof authClient.$Infer.Session)['user'] diff --git a/src/lib/constants.tsx b/src/lib/constants.tsx index 5ea5c1a..230e864 100644 --- a/src/lib/constants.tsx +++ b/src/lib/constants.tsx @@ -1,14 +1,14 @@ -import BetterAuth from '@/components/logos/better-auth' +import LogoMark from "@/components/logos/mark" -const APP_NAME = 'Better-Auth Starter' -const APP_DEFAULT_TITLE = 'Better-Auth Next.js Starter' -const APP_TITLE_TEMPLATE = '%s | Better-Auth Next.js Starter' +const APP_NAME = "Next.js Starter" +const APP_DEFAULT_TITLE = "Next.js Starter" +const APP_TITLE_TEMPLATE = "%s | Next.js Starter" const APP_DESCRIPTION = - 'Better-Auth Next.js Starter is a starter kit for building a Next.js application with Better-Auth' + "A minimal Next.js starter with Tailwind CSS and shadcn/ui." const logo = ( <> - + ) diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts index 5f6f0da..cb00de2 100644 --- a/src/lib/metadata.ts +++ b/src/lib/metadata.ts @@ -1,6 +1,6 @@ -import type { Metadata } from 'next' -import { env } from '@/env' -import { APP_DEFAULT_TITLE } from './constants' +import type { Metadata } from "next" +import { env } from "@/env" +import { APP_DEFAULT_TITLE } from "./constants" export function createMetadata(override: Metadata): Metadata { return { @@ -9,22 +9,21 @@ export function createMetadata(override: Metadata): Metadata { title: override.title ?? undefined, description: override.description ?? undefined, url: baseUrl.toString(), - images: '/banner.png', + images: "/banner.png", siteName: APP_DEFAULT_TITLE, ...override.openGraph, }, twitter: { - card: 'summary_large_image', - creator: '@AnirudhWith', + card: "summary_large_image", title: override.title ?? undefined, description: override.description ?? undefined, - images: '/banner.png', + images: "/banner.png", ...override.twitter, }, } } export const baseUrl = - env.NODE_ENV === 'development' || !env.NEXT_PUBLIC_BASE_URL - ? new URL('http://localhost:3000') - : new URL(`https://${env.NEXT_PUBLIC_BASE_URL}`) + env.NODE_ENV === "development" || !env.NEXT_PUBLIC_BASE_URL + ? new URL("http://localhost:3000") + : new URL(`${env.NEXT_PUBLIC_BASE_URL}`) diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index 83c19b9..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { getSessionCookie } from 'better-auth/cookies' -import { type NextRequest, NextResponse } from 'next/server' - -import { - DEFAULT_LOGIN_REDIRECT, - getSignInUrl, - isApiAuth, - isAuthRoute, - isPublicRoute, -} from '@/routes' - -export async function middleware(request: NextRequest) { - const session = getSessionCookie(request) - - const pathname = request.nextUrl.pathname - - if (isApiAuth(pathname)) { - return NextResponse.next() - } - - if (isAuthRoute(pathname)) { - if (session) { - return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, request.url)) - } - return NextResponse.next() - } - - if (!session && !isPublicRoute(pathname)) { - return NextResponse.redirect(getSignInUrl(request)) - } - - return NextResponse.next() -} - -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - * Feel free to modify this pattern to include more paths. - */ - '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', - ], -} diff --git a/src/routes.ts b/src/routes.ts deleted file mode 100644 index 70996cb..0000000 --- a/src/routes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { NextRequest } from 'next/server' - -export const PUBLIC_ROUTES: string[] = ['/about'] -export const AUTH_PREFIX: string = '/auth' -export const API_AUTH_PREFIX: string = '/api/auth' -export const DEFAULT_LOGIN_REDIRECT: string = '/' - -export function isApiAuth(pathname: string): boolean { - return startsWith(pathname, API_AUTH_PREFIX) -} - -export function isAuthRoute(pathname: string): boolean { - return startsWith(pathname, AUTH_PREFIX) -} - -export function isPublicRoute(pathname: string): boolean { - return isPathEqual(pathname, PUBLIC_ROUTES) -} - -// Utils -function isPathEqual(pathname: string, paths: string[]): boolean { - return paths.includes(pathname) -} - -function startsWith(pathname: string, prefix: string): boolean { - return pathname.startsWith(prefix) -} - -export function getSignInUrl(request: NextRequest, redirectTo?: string): URL { - const rt = redirectTo ?? request.nextUrl.pathname + request.nextUrl.search - return new URL( - `${AUTH_PREFIX}/sign-in?redirectTo=${encodeURIComponent(rt)}`, - request.url - ) -} diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts deleted file mode 100644 index 2906737..0000000 --- a/src/server/auth/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { betterAuth } from 'better-auth' -import { drizzleAdapter } from 'better-auth/adapters/drizzle' -import { anonymous } from 'better-auth/plugins' -import { headers } from 'next/headers' -import type { NextRequest } from 'next/server' -import { env } from '@/env' -import { db } from '@/server/db' -import * as schema from '@/server/db/schema' - -export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: 'pg', - usePlural: true, - schema, - }), - emailAndPassword: { - enabled: true, - }, - plugins: [anonymous()], - trustedOrigins: env.CORS_ORIGIN, - baseURL: env.NEXT_PUBLIC_BASE_URL, -}) - -export const getSession = async (request?: NextRequest) => { - return auth.api.getSession({ - headers: request ? request.headers : await headers(), - }) -} diff --git a/src/server/db/index.ts b/src/server/db/index.ts deleted file mode 100644 index 0b117d2..0000000 --- a/src/server/db/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { neon } from '@neondatabase/serverless' -import { drizzle } from 'drizzle-orm/neon-http' - -import { env } from '@/env' -import * as schema from './schema' - -const sql = neon(env.DATABASE_URL) - -export const db = drizzle({ - client: sql, - schema, - casing: 'snake_case', -}) diff --git a/src/server/db/schema/auth.ts b/src/server/db/schema/auth.ts deleted file mode 100644 index 8ec5d50..0000000 --- a/src/server/db/schema/auth.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core' - -export const users = pgTable('users', { - id: text('id').primaryKey(), - name: text('name').notNull(), - email: text('email').notNull().unique(), - emailVerified: boolean('email_verified').default(false).notNull(), - image: text('image'), - createdAt: timestamp('created_at') - .$defaultFn(() => new Date()) - .notNull(), - updatedAt: timestamp('updated_at') - .$defaultFn(() => new Date()) - .$onUpdate(() => new Date()) - .notNull(), - isAnonymous: boolean('is_anonymous'), -}) - -export const sessions = pgTable('sessions', { - id: text('id').primaryKey(), - expiresAt: timestamp('expires_at').notNull(), - token: text('token').notNull().unique(), - createdAt: timestamp('created_at') - .$defaultFn(() => new Date()) - .notNull(), - updatedAt: timestamp('updated_at') - .$onUpdate(() => new Date()) - .notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), -}) - -export const accounts = pgTable('accounts', { - id: text('id').primaryKey(), - accountId: text('account_id').notNull(), - providerId: text('provider_id').notNull(), - userId: text('user_id') - .notNull() - .references(() => users.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') - .$defaultFn(() => new Date()) - .notNull(), - updatedAt: timestamp('updated_at') - .$onUpdate(() => new Date()) - .notNull(), -}) - -export const verifications = pgTable('verifications', { - id: text('id').primaryKey(), - identifier: text('identifier').notNull(), - value: text('value').notNull(), - expiresAt: timestamp('expires_at').notNull(), - createdAt: timestamp('created_at') - .$defaultFn(() => new Date()) - .notNull(), - updatedAt: timestamp('updated_at') - .$defaultFn(() => new Date()) - .$onUpdate(() => new Date()) - .notNull(), -}) diff --git a/src/server/db/schema/index.ts b/src/server/db/schema/index.ts deleted file mode 100644 index f140b2e..0000000 --- a/src/server/db/schema/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auth' diff --git a/src/styles/custom.css b/src/styles/custom.css index 34f2c21..323520c 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -1,5 +1,4 @@ @import "tailwindcss-safe-area"; -@import "better-auth-ui/css"; @layer base { button:not(:disabled), @@ -32,4 +31,4 @@ html { font: -apple-system-body; } -} \ No newline at end of file +}