- Frontend: Next.js 15 with React 19.0
- Styling: TailwindCSS with shadcn/ui components
- Authentication: Supabase Auth
- Database: Supabase
- Payments: Stripe
- Email: Mailgun
-
Clone the repository:
git clone https://github.com/Francesco-Fera/next-starter.git cd next-starter -
Install dependencies:
npm install --legacy-peer-deps
-
Set up environment variables:
- Create a
.env.localfile in the root directory. - Add the following variables, ensuring they match your Supabase, Stripe, and Mailgun credentials:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key STRIPE_SECRET_KEY=your_stripe_secret_key MAILGUN_API_KEY=your_mailgun_api_key MAILGUN_DOMAIN=your_mailgun_domain
- Create a
-
Start the development server:
npm run dev
Ensure you have added your Supabase credentials in .env.local:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_keysrc/
├── app/
│ ├── (auth)/
│ │ ├── callback/ # OAuth callback handling
│ │ ├── login/ # Login page
│ │ ├── register/ # Registration page
│ │ └── layout.tsx # Auth layout with session check
├── components/
│ ├── login-form.tsx # Login form component
│ └── register-form.tsx # Registration form component
└── lib/
└── supabase/
├── client.ts # Browser client initialization
└── server.ts # Server client initialization
// Login
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
// Registration
const { error } = await supabase.auth.signUp({
email,
password,
});const { error } = await supabase.auth.signInWithOtp({
email,
});const { error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});const handleSignOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
redirect("/auth/login");
};const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
// User is authenticated
} else {
// User is not authenticated
}// app/layout.tsx
export const metadata: Metadata = {
title: "Your App Name",
description: "Your app description",
keywords: ["keyword1", "keyword2"],
};export const metadata: Metadata = {
openGraph: {
type: "website",
locale: "en_US",
url: "https://yourdomain.com",
siteName: "Your App Name",
images: [
{
url: "https://yourdomain.com/og-image.jpg",
width: 1200,
height: 630,
alt: "Your App Name",
},
],
},
};// app/sitemap.ts
export default async function sitemap() {
const routes = ["", "/about", "/features", "/pricing"];
return routes.map((route) => ({
url: `https://yourdomain.com${route}`,
lastModified: new Date(),
}));
}export default async function sitemap() {
const posts = await fetchPosts();
const postUrls = posts.map((post) => ({
url: `https://yourdomain.com/blog/${post.slug}`,
lastModified: post.updatedAt,
}));
return [...postUrls];
}Ensure the following environment variables are set:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...import { stripe } from "@/lib/stripe";
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000,
currency: "usd",
payment_method_types: ["card"],
});const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: "price_H5ggYwtDq4fbrJ" }],
payment_behavior: "default_incomplete",
payment_settings: { save_default_payment_method: "on_subscription" },
expand: ["latest_invoice.payment_intent"],
});// app/api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";
import { headers } from "next/headers";
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get("Stripe-Signature") as string;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
return new Response(null, { status: 200 });
}MIT License