apps/docs/content/docs/guides/integrations/embed-studio.mdx
Prisma Studio can be embedded directly into your Next.js application using the @prisma/studio-core package. This guide walks you through the setup so you can manage your database from within your app instead of running Prisma Studio separately.
After completing the guide, you'll have a Next.js app with Prisma Studio embedded, allowing you to browse and edit your database directly from your application interface:
Embedding Prisma Studio can be useful in scenarios such as:
:::note
Embeddable Prisma Studio is free and licensed under Apache 2.0.
✔️ Free to use in production ⚠️ Prisma branding must remain visible and unchanged 🔐 To remove branding or learn about upcoming partner-only features, reach out at [email protected]
Currently, Embedded Prisma Studio supports PostgreSQL, SQLite, and MySQL databases.
:::
First, create a new Next.js project from the directory where you want to build your app:
npx create-next-app@latest nextjs-studio-embed
You will be prompted to answer a few questions about your project. Select all of the defaults.
:::info
For reference, those are:
src directory:::
Then, navigate to the project directory:
cd nextjs-studio-embed
Install the required Prisma packages:
npm install prisma tsx @types/pg --save-dev
npm install @prisma/extension-accelerate @prisma/client @prisma/adapter-pg dotenv pg
npm install prisma tsx --save-dev
npm install @prisma/client @prisma/adapter-sqlite dotenv better-sqlite3
npm install prisma tsx @types/mysql2 --save-dev
npm install @prisma/client @prisma/adapter-mysql dotenv mysql2
:::info
For more information, see Database drivers.
:::
Initialize Prisma in your project and create a Prisma Postgres database:
npx prisma init --db --output ../app/generated/prisma
npx prisma init --datasource-provider sqlite --output ../app/generated/prisma
npx prisma init --datasource-provider mysql --output ../app/generated/prisma
:::info
You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My **__** Project"
:::
The prisma init command creates:
prisma/ directory with your schema.prisma fileprisma.config.ts file for configuring Prisma.env file with your DATABASE_URLapp/generated/prisma for the Prisma ClientOpen prisma/schema.prisma and replace the content with:
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql" // this will change depending on the --datasource-provider flag used in the init command
}
// [!code ++]
model User { // [!code ++]
id Int @id @default(autoincrement()) // [!code ++]
name String // [!code ++]
email String @unique // [!code ++]
posts Post[] // [!code ++]
} // [!code ++]
// [!code ++]
model Post { // [!code ++]
id Int @id @default(autoincrement()) // [!code ++]
title String // [!code ++]
content String? // [!code ++]
published Boolean @default(false) // [!code ++]
authorId Int // [!code ++]
author User @relation(fields: [authorId], references: [id]) // [!code ++]
createdAt DateTime @default(now()) // [!code ++]
} // [!code ++]
// [!code ++]
dotenv to prisma.config.tsTo get access to the variables in the .env file, they can either be loaded by your runtime, or by using dotenv.
Include an import for dotenv at the top of the prisma.config.ts
import "dotenv/config"; // [!code ++]
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});
Generate the Prisma Client and apply the schema:
npx prisma migrate dev --name init
npx prisma generate
This creates the tables in your Prisma Postgres database and generates the Prisma Client.
:::info
If you are using SQLite or MySQL, this creates the tables in your database and generates the Prisma Client.
:::
Create a seed file to add some sample data. Create a seed.ts file in the prisma folder and add the following code:
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const prisma = new PrismaClient({
adapter,
});
async function main() {
// Create users
const user1 = await prisma.user.create({
data: {
name: "Alice Johnson",
email: "[email protected]",
},
});
const user2 = await prisma.user.create({
data: {
name: "Bob Smith",
email: "[email protected]",
},
});
// Create posts
await prisma.post.create({
data: {
title: "Getting Started with Next.js",
content: "Next.js is a powerful React framework...",
published: true,
authorId: user1.id,
},
});
await prisma.post.create({
data: {
title: "Database Management with Prisma",
content: "Prisma makes database management easy...",
published: false,
authorId: user2.id,
},
});
console.log("Database seeded successfully!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaSQLite } from "@prisma/adapter-sqlite";
const adapter = new PrismaSQLite({
connectionString: process.env.DATABASE_URL!,
});
const prisma = new PrismaClient({
adapter,
});
async function main() {
// Create users
const user1 = await prisma.user.create({
data: {
name: "Alice Johnson",
email: "[email protected]",
},
});
const user2 = await prisma.user.create({
data: {
name: "Bob Smith",
email: "[email protected]",
},
});
// Create posts
await prisma.post.create({
data: {
title: "Getting Started with Next.js",
content: "Next.js is a powerful React framework...",
published: true,
authorId: user1.id,
},
});
await prisma.post.create({
data: {
title: "Database Management with Prisma",
content: "Prisma makes database management easy...",
published: false,
authorId: user2.id,
},
});
console.log("Database seeded successfully!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaMySQL } from "@prisma/adapter-mysql";
const adapter = new PrismaMySQL({
connectionString: process.env.DATABASE_URL!,
});
const prisma = new PrismaClient({
adapter,
});
async function main() {
// Create users
const user1 = await prisma.user.create({
data: {
name: "Alice Johnson",
email: "[email protected]",
},
});
const user2 = await prisma.user.create({
data: {
name: "Bob Smith",
email: "[email protected]",
},
});
// Create posts
await prisma.post.create({
data: {
title: "Getting Started with Next.js",
content: "Next.js is a powerful React framework...",
published: true,
authorId: user1.id,
},
});
await prisma.post.create({
data: {
title: "Database Management with Prisma",
content: "Prisma makes database management easy...",
published: false,
authorId: user2.id,
},
});
console.log("Database seeded successfully!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Add a seed script to your prisma.config.ts:
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: `tsx prisma/seed.ts`,
},
datasource: {
url: env("DIRECT_URL"),
},
});
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: `tsx prisma/seed.ts`,
},
datasource: {
url: env("DATABASE_URL"),
},
});
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: `tsx prisma/seed.ts`,
},
datasource: {
url: env("DATABASE_URL"),
},
});
Run the seed script:
npx prisma db seed
Now that you have Prisma ORM and Prisma Postgres set up, you can embed Prisma Studio in your Next.js app.
Install the @prisma/studio-core package that provides the embeddable components:
npm install @prisma/studio-core
:::note
If you encounter a dependency resolution error while installing @prisma/studio-core, you can force the install with:
npm install @prisma/studio-core --force
If you are using yarn, pnpm, or another package manager, use the equivalent flag for your tool. :::
The @prisma/studio-core provides Studio, a React component which renders Prisma Studio for your database. The Studio component accepts an executor which accesses a custom endpoint in your backend. The backend uses your API key to identify the correct Prisma Postgres instance and sends the SQL query to it.
Create a components folder and add a new file called StudioWrapper.tsx. This file will wrap the Studio component and provide a consistent layout:
"use client";
import "@prisma/studio-core/ui/index.css";
import { ReactNode } from "react";
interface StudioWrapperProps {
children: ReactNode;
}
export default function StudioWrapper({ children }: StudioWrapperProps) {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<h1 className="text-2xl font-bold text-gray-900">Database Studio</h1>
<div className="text-sm text-gray-500">Powered by Prisma Studio</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto">
<div className="h-[calc(100vh-80px)]">{children}</div>
</main>
</div>
);
}
Next, set up a backend endpoint that Prisma Studio can communicate with. This endpoint receives SQL queries from the embedded Studio UI, forwards them to your Prisma Postgres database, and then returns the results (or errors) back to the frontend.
:::info
If you are using SQLite or MySQL, you can still embed Studio, but your /api/studio implementation needs to use the correct executor for your database.
:::
To do this, create a new folder called api inside the app directory. Inside it, add a studio folder with a route.ts file. This file will handle all requests sent to /api/studio and act as the bridge between the Studio component in your frontend and the database in your backend:
import "dotenv/config";
import { createPrismaPostgresHttpClient } from "@prisma/studio-core/data/ppg";
import { serializeError } from "@prisma/studio-core/data/bff";
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*", // Change to your domain in production
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
// Use dynamic rendering for database operations
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json({ message: "Studio API endpoint is running" }, { headers: CORS_HEADERS });
}
export async function POST(request: Request) {
try {
const body = await request.json();
const query = body.query;
if (!query) {
return Response.json([serializeError(new Error("Query is required"))], {
status: 400,
headers: CORS_HEADERS,
});
}
const url = process.env.DATABASE_URL;
if (!url) {
const message = "❌ Environment variable DATABASE_URL is missing.";
return Response.json([serializeError(new Error(message))], {
status: 500,
headers: CORS_HEADERS,
});
}
const [error, results] = await createPrismaPostgresHttpClient({
url,
}).execute(query);
if (error) {
return Response.json([serializeError(error)], {
headers: CORS_HEADERS,
});
}
return Response.json([null, results], { headers: CORS_HEADERS });
} catch (err) {
return Response.json([serializeError(err)], {
status: 400,
headers: CORS_HEADERS,
});
}
}
// Handle preflight requests for CORS
export async function OPTIONS() {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
import "dotenv/config";
import { createNodeSQLiteExecutor } from "@prisma/studio-core/data/node-sqlite";
import { serializeError } from "@prisma/studio-core/data/bff";
import DatabaseSync from "better-sqlite3";
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*", // Change to your domain in production
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
// Use dynamic rendering for database operations
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json({ message: "Studio API endpoint is running" }, { headers: CORS_HEADERS });
}
export async function POST(request: Request) {
try {
const body = await request.json();
const query = body.query;
if (!query) {
return Response.json([serializeError(new Error("Query is required"))], {
status: 400,
headers: CORS_HEADERS,
});
}
const url = process.env.DATABASE_URL;
if (!url) {
const message = "❌ Environment variable DATABASE_URL is missing.";
return Response.json([serializeError(new Error(message))], {
status: 500,
headers: CORS_HEADERS,
});
}
const dbPath = url.replace("file:", "");
const database = new DatabaseSync(dbPath);
const [error, results] = await createNodeSQLiteExecutor(database).execute(query);
if (error) {
return Response.json([serializeError(error)], {
headers: CORS_HEADERS,
});
}
return Response.json([null, results], { headers: CORS_HEADERS });
} catch (err) {
return Response.json([serializeError(err)], {
status: 400,
headers: CORS_HEADERS,
});
}
}
// Handle preflight requests for CORS
export async function OPTIONS() {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
import "dotenv/config";
import { createMySQL2Executor } from "@prisma/studio-core/data/mysql2";
import { serializeError } from "@prisma/studio-core/data/bff";
import mysql from "mysql2/promise";
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*", // Change to your domain in production
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
// Use dynamic rendering for database operations
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json({ message: "Studio API endpoint is running" }, { headers: CORS_HEADERS });
}
export async function POST(request: Request) {
try {
const body = await request.json();
const query = body.query;
if (!query) {
return Response.json([serializeError(new Error("Query is required"))], {
status: 400,
headers: CORS_HEADERS,
});
}
const url = process.env.DATABASE_URL;
if (!url) {
const message = "❌ Environment variable DATABASE_URL is missing.";
return Response.json([serializeError(new Error(message))], {
status: 500,
headers: CORS_HEADERS,
});
}
const pool = mysql.createPool(url);
const [error, results] = await createMySQL2Executor(pool).execute(query);
if (error) {
return Response.json([serializeError(error)], {
headers: CORS_HEADERS,
});
}
return Response.json([null, results], { headers: CORS_HEADERS });
} catch (err) {
return Response.json([serializeError(err)], {
status: 400,
headers: CORS_HEADERS,
});
}
}
// Handle preflight requests for CORS
export async function OPTIONS() {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
Open the app/page.tsx file and replace the existing code to render the embedded Studio with the following:
"use client";
import dynamic from "next/dynamic";
import { createPostgresAdapter } from "@prisma/studio-core/data/postgres-core";
import { createStudioBFFClient } from "@prisma/studio-core/data/bff";
import { useMemo, Suspense } from "react";
import StudioWrapper from "@/components/StudioWrapper";
// Dynamically import Studio with no SSR to avoid hydration issues
const Studio = dynamic(() => import("@prisma/studio-core/ui").then((mod) => mod.Studio), {
ssr: false,
});
// Loading component
const StudioLoading = () => (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading Studio...</p>
</div>
</div>
);
// Client-only Studio component
const ClientOnlyStudio = () => {
const adapter = useMemo(() => {
// Create the HTTP client that communicates with our API endpoint
const executor = createStudioBFFClient({
url: "/api/studio",
});
// Create the Postgres adapter using the executor
return createPostgresAdapter({ executor });
}, []);
return <Studio adapter={adapter} />;
};
export default function App() {
return (
<StudioWrapper>
<Suspense fallback={<StudioLoading />}>
<ClientOnlyStudio />
</Suspense>
</StudioWrapper>
);
}
"use client";
import dynamic from "next/dynamic";
import { createSQLiteAdapter } from "@prisma/studio-core/data/sqlite-core";
import { createStudioBFFClient } from "@prisma/studio-core/data/bff";
import { useMemo, Suspense } from "react";
import StudioWrapper from "@/components/StudioWrapper";
// Dynamically import Studio with no SSR to avoid hydration issues
const Studio = dynamic(() => import("@prisma/studio-core/ui").then((mod) => mod.Studio), {
ssr: false,
});
// Loading component
const StudioLoading = () => (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading Studio...</p>
</div>
</div>
);
// Client-only Studio component
const ClientOnlyStudio = () => {
const adapter = useMemo(() => {
// Create the HTTP client that communicates with our API endpoint
const executor = createStudioBFFClient({
url: "/api/studio",
});
// Create the SQLite adapter using the executor
return createSQLiteAdapter({ executor });
}, []);
return <Studio adapter={adapter} />;
};
export default function App() {
return (
<StudioWrapper>
<Suspense fallback={<StudioLoading />}>
<ClientOnlyStudio />
</Suspense>
</StudioWrapper>
);
}
"use client";
import dynamic from "next/dynamic";
import { createMySQLAdapter } from "@prisma/studio-core/data/mysql-core";
import { createStudioBFFClient } from "@prisma/studio-core/data/bff";
import { useMemo, Suspense } from "react";
import StudioWrapper from "@/components/StudioWrapper";
// Dynamically import Studio with no SSR to avoid hydration issues
const Studio = dynamic(() => import("@prisma/studio-core/ui").then((mod) => mod.Studio), {
ssr: false,
});
// Loading component
const StudioLoading = () => (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading Studio...</p>
</div>
</div>
);
// Client-only Studio component
const ClientOnlyStudio = () => {
const adapter = useMemo(() => {
// Create the HTTP client that communicates with our API endpoint
const executor = createStudioBFFClient({
url: "/api/studio",
});
// Create the MySQL adapter using the executor
return createMySQLAdapter({ executor });
}, []);
return <Studio adapter={adapter} />;
};
export default function App() {
return (
<StudioWrapper>
<Suspense fallback={<StudioLoading />}>
<ClientOnlyStudio />
</Suspense>
</StudioWrapper>
);
}
Start your Next.js development server:
npm run dev
Open your browser and go to http://localhost:3000. You should now see Prisma Studio running inside your application:
Here's what to look for:
User and Post tables you defined (plus any seeded data) should appear.Verify whether everything works by testing the basics:
Once these actions work as expected, your embedded Prisma Studio is set up and connected to your database.
At this point you have Prisma Studio running inside your Next.js application and connected to your database. You can browse, edit, and manage your data without leaving your app. To make this setup production-ready, consider these improvements:
Add authentication: Currently, anyone who can open your app has access to Prisma Studio. Add user authentication and only allow specific roles (for example, admins) to use the embedded Studio. You can do this by checking authentication tokens in your /api/studio endpoint before running queries.
Use environment-specific configuration: In development you may want a test database, while in production you'll need a separate live database. Update your .env file to use different DATABASE_URL values for each environment, and confirm that your /api/studio endpoint is reading the correct one.
Apply custom styling: The Studio component ships with a default look. Pass in your own theme and adjust colors, typography, or branding to match the rest of your application. This helps Studio feel like a native part of your app rather than a standalone tool.
By adding authentication, environment-specific settings, and styling, you move from a working demo to a secure and polished production setup.
For more patterns and examples, see the Prisma Studio Core demo repository, which includes an alternative implementation using Hono.js and React. If you prefer a guided walkthrough, watch the YouTube video: **Use Prisma Studio in Your Own Applications **.