Published Jun 19, 2025
In this series, I’ll walk you through building Reviewly, a comprehensive review management platform that helps businesses collect, manage, and showcase authentic customer testimonials. This first post covers the foundation and initial setup.
Reviewly aims to solve a common problem: businesses losing potential customers due to poor review management. The platform provides:
After evaluating various options, I chose a modern, scalable stack:
npx create-next-app@latest review-platform --typescript --tailwind --app --src-dir
This gave us a solid foundation with:
The heart of any application is its data model. I designed a comprehensive schema using Prisma:
model User {
id String @id @default(cuid())
email String @unique
password String?
name String?
role String // 'admin', 'client'
clients Client[] @relation("UserClients")
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Client {
id String @id @default(cuid())
name String
domain String
brandingConfig Json
subscriptionTier String
apiKeys Json
reviews Review[]
createdBy User @relation("UserClients", fields: [createdById], references: [id])
createdById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Review {
id String @id @default(cuid())
userId String
clientId String
rating Int
content String
mediaUrls Json?
status String
client Client @relation(fields: [clientId], references: [id])
media Media[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Key design decisions:
NextAuth.js provides a robust authentication solution. I configured it with:
const authOptions = {
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
isSignUp: { label: "Sign Up", type: "hidden" },
name: { label: "Name", type: "text", optional: true },
},
async authorize(credentials) {
// Handle both sign-in and sign-up in one provider
if (credentials.isSignUp === "true") {
// Create new user
} else {
// Authenticate existing user
}
},
}),
],
session: { strategy: "jwt" as SessionStrategy },
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
};
This setup allows for:
The landing page needed to be compelling and conversion-focused. I created a modern design with:
<h1 className="text-5xl sm:text-7xl font-extrabold mb-6 leading-tight">
<span className="bg-gradient-to-r from-white via-slate-200 to-slate-300 bg-clip-text text-transparent">
Transform customer
</span>
<br />
<span className="bg-gradient-to-r from-violet-400 via-purple-400 to-cyan-400 bg-clip-text text-transparent">
feedback into growth
</span>
</h1>
I implemented route protection using Next.js App Router and server components:
export default async function DashboardPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/auth/signin");
}
return (
<div>
<h1>Dashboard</h1>
<p>Signed in as {session.user?.email}</p>
{/* Dashboard content */}
</div>
);
}
This ensures that:
I added several tools to maintain code quality:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["next lint"]
},
"scripts": {
"prepare": "husky"
}
}
Using Prisma migrations for database schema changes:
npx prisma migrate dev --name init
npx prisma generate
This ensures:
At this point, we have:
✅ Foundation: Next.js app with TypeScript and Tailwind
✅ Database: Prisma schema with PostgreSQL
✅ Authentication: NextAuth.js with custom sign-in/sign-up
✅ Landing Page: Modern, conversion-focused design
✅ Protected Routes: Server-side authentication
✅ Code Quality: Linting and formatting setup
In the next post, I’ll cover:
The foundation is solid, and we’re ready to build the core features that will make Reviewly a powerful review management platform.
Stay tuned for Part 2, where we’ll dive into building the dashboard and review management system!