DEV Community

Cover image for NEXTJS Implimentation Prisma - Supabase
SAMBIT
SAMBIT

Posted on Edited on AI-assisted

NEXTJS Implimentation Prisma - Supabase

Build a Full-Stack Blog Editor with Next.js, Prisma 7, and Supabase

In this tutorial, we'll set up a simple full-stack blog editor using Next.js App Router, Prisma 7, and Supabase PostgreSQL.

You'll learn how to connect Prisma 7 to Supabase, define a blog post model, create a Prisma Client singleton, and save posts using a Next.js Server Action.

🛠️ Tech Stack

  • Framework: Next.js (App Router)
  • ORM: Prisma 7
  • Database: Supabase (PostgreSQL)
  • PostgreSQL driver: pg
  • Prisma adapter: @prisma/adapter-pg
  • Styling: Tailwind CSS

1. Create a Next.js project

If you don't already have a project, create one:

npx create-next-app@latest my-blog
cd my-blog
Enter fullscreen mode Exit fullscreen mode

Choose the options you prefer. This example uses the src directory.

2. Install dependencies

Prisma 7 uses a database driver adapter at runtime. Install the Prisma Client, PostgreSQL driver, and adapter:

npm install @prisma/client@7 @prisma/adapter-pg pg dotenv
npm install -D prisma@7
Enter fullscreen mode Exit fullscreen mode

3. Configure Supabase environment variables

Create a .env file in your project root.

In your Supabase dashboard, open Project Settings → Database and copy the connection details.

# Runtime connection: Supabase connection pooler
DATABASE_URL="postgresql://postgres.[PROJECT-REF]:[PASSWORD]@[POOLER-HOST]:6543/postgres?pgbouncer=true"

# Migration connection: direct database connection
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
Enter fullscreen mode Exit fullscreen mode

Replace the placeholders with your actual Supabase values. Depending on your network and Supabase configuration, you may need to use the session pooler for migrations instead of the direct database host.

Important: Never commit .env or expose database connection strings in client-side code. Keep credentials private.

4. Configure Prisma 7

Create prisma.config.ts in the project root:

import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    // Used by Prisma Migrate
    url: env("DIRECT_URL"),
  },
});
Enter fullscreen mode Exit fullscreen mode

In Prisma 7, the database connection URL is configured in prisma.config.ts, rather than inside the datasource block in schema.prisma.

5. Define your Prisma schema

Create or update prisma/schema.prisma:

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  videoLink String?
  tags      String[]
  published Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
Enter fullscreen mode Exit fullscreen mode

A few things to notice:

  • id is the unique primary key for each post.
  • videoLink is optional, so it can be null.
  • tags is a PostgreSQL string array.
  • published distinguishes drafts from published posts.
  • createdAt and updatedAt track when a post was created and last updated.

6. Run the migration and generate Prisma Client

Run:

npx prisma migrate dev --name init
npx prisma generate
Enter fullscreen mode Exit fullscreen mode

This creates the database table in Supabase and generates the Prisma Client in src/generated/prisma.

7. Create a Prisma Client singleton

Create src/lib/prisma.ts:

import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
  throw new Error("DATABASE_URL is not set");
}

const adapter = new PrismaPg({ connectionString });

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ?? new PrismaClient({ adapter });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}
Enter fullscreen mode Exit fullscreen mode

The singleton pattern reuses the Prisma Client during Next.js development hot reloads, helping prevent unnecessary database connections.

8. Create a Server Action

Create src/app/actions.ts:

"use server";

import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function createPost(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  const content = String(formData.get("content") ?? "").trim();
  const videoLink = String(formData.get("videoLink") ?? "").trim();
  const tagsString = String(formData.get("tags") ?? "");
  const actionType = String(formData.get("actionType") ?? "draft");

  if (!title || !content) {
    throw new Error("Title and content are required.");
  }

  const tags = tagsString
    .split(",")
    .map((tag) => tag.trim())
    .filter(Boolean);

  await prisma.post.create({
    data: {
      title,
      content,
      videoLink: videoLink || null,
      tags,
      published: actionType === "publish",
    },
  });

  revalidatePath("/");
  redirect("/");
}
Enter fullscreen mode Exit fullscreen mode

This Server Action:

  1. Reads the submitted form values.
  2. Validates the required title and content.
  3. Converts comma-separated tags into a string array.
  4. Saves the post as a draft or publishes it.
  5. Revalidates the home page and redirects.

For a production application, also add authentication, authorization, and robust input validation. A Server Action is an entry point and should not be treated as automatically authorized.

9. Build the blog editor UI

Create src/app/create/page.tsx:

import { createPost } from "@/app/actions";

export default function CreatePostPage() {
  return (
    <main className="mx-auto max-w-2xl p-8">
      <h1 className="mb-6 text-3xl font-bold">Create a blog post</h1>

      <form action={createPost} className="flex flex-col gap-4">
        <input
          type="text"
          name="title"
          placeholder="Post title"
          required
          className="rounded-lg border p-3"
        />

        <textarea
          name="content"
          placeholder="Write your post..."
          required
          rows={10}
          className="rounded-lg border p-3"
        />

        <input
          type="url"
          name="videoLink"
          placeholder="Optional video URL"
          className="rounded-lg border p-3"
        />

        <input
          type="text"
          name="tags"
          placeholder="Tags, separated by commas"
          className="rounded-lg border p-3"
        />

        <div className="flex gap-3">
          <button
            type="submit"
            name="actionType"
            value="draft"
            className="rounded-lg border px-5 py-2"
          >
            Save draft
          </button>

          <button
            type="submit"
            name="actionType"
            value="publish"
            className="rounded-lg bg-black px-5 py-2 text-white"
          >
            Publish post
          </button>
        </div>
      </form>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

The form submits directly to the Server Action. The selected button determines whether the post is saved as a draft or published.

10. Display saved posts

Create or update src/app/page.tsx:

import { prisma } from "@/lib/prisma";

export default async function HomePage() {
  const posts = await prisma.post.findMany({
    orderBy: {
      createdAt: "desc",
    },
  });

  return (
    <main className="mx-auto max-w-3xl p-8">
      <h1 className="mb-6 text-3xl font-bold">My Blog</h1>

      {posts.length === 0 ? (
        <p className="text-gray-500">No posts yet.</p>
      ) : (
        <div className="space-y-4">
          {posts.map((post) => (
            <article key={post.id} className="rounded-lg border p-5">
              <h2 className="text-xl font-semibold">{post.title}</h2>

              <p className="mt-2 whitespace-pre-wrap">{post.content}</p>

              <p className="mt-3 text-sm text-gray-500">
                {post.published ? "Published" : "Draft"}
              </p>

              {post.tags.length > 0 && (
                <p className="mt-2 text-sm">Tags: {post.tags.join(", ")}</p>
              )}
            </article>
          ))}
        </div>
      )}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

11. Start the development server

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 and visit /create to write your first post.


⚠️ Important: Supabase Row Level Security (RLS)

When Prisma connects directly to PostgreSQL using a database connection string, queries do not automatically go through Supabase's PostgREST API. Therefore, Supabase Row Level Security policies are not automatically applied to these Prisma queries.

Protect your Server Actions with authentication and authorization, validate user input, and keep database credentials on the server. Do not assume that enabling RLS alone protects queries made through a privileged direct database connection.

Top comments (0)