Introduction
Welcome to Plainform
Plainform is a production-ready Next.js starter kit that helps developers launch fullstack SaaS (Software as a Service) apps faster, with all essential integrations pre-configured.
Provides a solid foundation to quickly launch a SaaS application, eliminating the needs to build common features that every application needs.
Key features
Email verification, passkey management, social account linking/unlinking, and more.
Subscriptions and one-time payments instantly with Stripe integration.
Supabase, PostgreSQL, Prisma, and AWS S3, providing a robust and scalable backend with reliable database and file storage.
Ensure your transactional and marketing emails reach real inboxes, not spam folders.
Boost your Google rankings with complete technical SEO out of the box.
Easily editable Markdown (MDX) files for simple, flexible content management.
Ready-to-use, customizable UI components built with Shadcn UI and Tailwind CSS for fast, beautiful interfaces.
Built with cutting-edge technologies
Plainform equips you with everything needed to build and scale a modern SaaS application. From a dynamic blog to secure auth, payments, and file management, it’s pre-configured and ready to deploy on Vercel, Azure, or your preferred platform.
Secure Authentication with Clerk
Plainform integrates Clerk for seamless authentication, supporting email/password, social logins, and session management.
- Pre-configured for sign-in, sign-up, and protected routes.
- Redirects signed-in users from auth pages to the homepage.
- Scalable session handling for secure user management.
const onSubmit: SubmitHandler<IFormData> = async (data) => {
const { identifier, password } = data;
if (!isLoaded) return;
try {
const signInAttempt = await signIn.create({
identifier,
password,
});
if (signInAttempt.status === 'complete') {
await setActive({ session: signInAttempt.createdSessionId });
router.push('/');
} else {
return null;
}
} catch (err: Error) {
err.errors.forEach((error: ClerkAPIError) => {
const paramName = error.meta?.paramName as keyof IFormData | undefined;
if (paramName && error.longMessage) {
setError(paramName, {
type: 'manual',
message: error.longMessage,
});
} else if (paramName === undefined) {
toast.error(error?.message);
}
});
}
};Production-Ready Payments
Monetize your app with Stripe, including subscriptions, webhooks, and manual payment capture.
- Supports one-time and recurring payments with minimal setup.
- Manual capture allows conditional payment processing.
- API endpoints like /api/orders for tracking purchases.
export async function capturePaymentIntent(paymentIntentId: string): Promise<{
ok: boolean;
message: string;
additional_message: string;
order_status: string;
status?: number;
}> {
try {
const paymentIntent = await stripe.paymentIntents.capture(paymentIntentId);
if (paymentIntent.status !== 'succeeded') {
return {
ok: false,
order_status: 'Payment failed',
message: 'We failed to capture the payment.',
additional_message:
'Please try again or contact us if the issue persists!',
status: 400,
};
}
return {
ok: true,
order_status: 'Payment accepted',
message: `We captured the payment.`,
additional_message: 'Thank you for choosing us. Start building!',
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return {
ok: false,
order_status: 'Something went wrong',
message: `Payment capture error.`,
additional_message:
'Please try again or contact us if the issue persists!',
status: error.status || 400,
};
}
}Scalable Backend with Supabase
Plainform uses Supabase, a PostgreSQL-based backend, with Prisma ORM for type-safe and AWS S3 as the storage.
- Fully type-safe queries with TypeScript for reliable data handling.
- Real-time capabilities and easy schema migrations for dynamic apps.
- Scales seamlessly for growing user bases and complex data needs.
export async function GET() {
try {
const event = await prisma.event.findFirst({
take: 1,
orderBy: {
timestamp: 'desc',
},
});
if (!event) {
return NextResponse.json({ event: null }, { status: 404 });
}
const serializedEvent = {
...event,
timestamp: serializeBigInt(event.timestamp),
slug: event.type === 'post' ? `blog/${event.slug}` : event.slug,
};
return NextResponse.json({ event: serializedEvent }, { status: 200 });
} catch (error) {
console.error(error);
return NextResponse.json({ message: 'Server error' }, { status: 500 });
}
}Reliable Emails
Send transactional and marketing emails using Resend’s simple, reliable API.
- Unified API for welcome emails, password resets, and notifications.
- High deliverability with Resend’s optimized infrastructure.
- Easy integration for automated email workflows.
export async function POST(req: NextRequest) {
resend.contacts.create({
email: email,
unsubscribed: false,
audienceId: process.env.RESEND_AUDIENCE_ID as string,
});
}SEO Optimized
Plainform includes all the SEO essentials to boost your Google rankings right out of the box.
- Automatically generated XML sitemaps (
/sitemap.xml) help search engines discover and index all your pages efficiently. - Dynamic title, description, and OpenGraph tags improve click-through rates and social sharing.
- Next.js SSR, static generation, and image optimization ensure fast load times, meeting Google’s Core Web Vitals for better rankings.
export async function generateMetadata({ params }: IBlogParams) {
const { slug } = await params;
const data = await getBlogPost(slug);
const { post } = data;
return {
title: post?.metadata?.title,
description: post?.metadata?.summary,
openGraph: {
title: post?.metadata?.title,
description: post?.metadata?.summary,
url: process.env.NEXT_PUBLIC_URL,
siteName: post?.metadata?.title,
images: [
{
url: post?.metadata?.img,
width: 1280,
height: 628,
},
],
locale: siteConfig.locale,
type: siteConfig.type,
},
};
}MDX Blog
Unlike database-driven blogs, MDX Blog uses easily editable Markdown (MDX) files for simple, flexible content management.
- Blend Markdown and JSX for flexible, engaging content.
- Because MDX compiles during the build stage rather than at runtime, it delivers blazing-fast performance.
- Enhances SEO with pre-rendered content and dynamic metadata generation.
export function CustomMDX(props: any) {
return (
<MDXRemote
{...props}
components={{ ...components, ...(props.components || {}) }}
options={{
mdxOptions: {
rehypePlugins: [rehypeMdxCodeProps],
},
}}
/>
);
}UI Components
Build responsive, beautiful interfaces with Shadcn/UI components, powered by Tailwind CSS.
- Pre-built components (tabs, dropdowns, modals) speed up UI development.
- Responsive design with dark mode support ensures accessibility.
- Fully customizable with Tailwind for a cohesive look.
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };Folder Structure
We tried to make the folder structure as easy as possible for you to understand and to be efficient.
package.json
Here is the entire package.json file for the project so you can make an idea of what we used.
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prettier": "prettier --write .",
"prepare": "husky",
"postinstall": "prisma generate && fumadocs-mdx",
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe/webhook",
"stripe:trigger": "stripe trigger checkout.session.async_payment_failed,checkout.session.async_payment_succeeded,checkout.session.completed,checkout.session.expired,coupon.created,coupon.deleted,coupon.updated,product.created,product.deleted,product.updated"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.848.0",
"@aws-sdk/s3-request-presigner": "^3.848.0",
"@clerk/nextjs": "^6.22.0",
"@clerk/types": "^4.60.1",
"@hookform/resolvers": "^5.1.1",
"@prisma/client": "^6.14.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-visually-hidden": "^1.2.3",
"@stripe/stripe-js": "^7.4.0",
"@types/mdx": "^2.0.13",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.1",
"fumadocs-core": "^15.7.1",
"fumadocs-mdx": "^11.8.0",
"fumadocs-ui": "^15.7.1",
"gray-matter": "^4.0.3",
"input-otp": "^1.4.2",
"lucide-react": "^0.511.0",
"mdast-util-from-markdown": "^2.0.2",
"motion": "^12.16.0",
"next": "15.3.2",
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.58.1",
"react-spinners": "^0.17.0",
"react-tweet": "^3.2.2",
"reading-time": "^1.5.0",
"rehype-mdx-code-props": "^3.0.1",
"resend": "^4.7.0",
"sonner": "^2.0.5",
"stripe": "^18.3.0",
"sugar-high": "^0.9.3",
"tailwind-merge": "^3.3.0",
"zod": "^3.25.56",
"zustand": "^5.0.8"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@eslint/eslintrc": "^3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"husky": "^9.1.7",
"prettier": "^3.5.3",
"prisma": "^6.14.0",
"semantic-release": "^24.2.5",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.0",
"typescript": "^5"
}How is this guide ?
Last updated on