In this blog post, we'll be focusing on how to integrate Next.js and Prisma, two popular technologies for building modern web applications, with Cerbos, the open-source, decoupled authorization layer for your software.
By combining the power of Next.js for creating fast, server-rendered React applications with the flexible, data-driven capabilities of Prisma, you can create dynamic and secure web experiences for your users. By integrating an authorization layer, you can control access to your application's resources, providing an extra layer of security and protection for your data. Join us as we explore this integration and discover how you can elevate your Next.js applications to the next level.
You can check out the code for this demo here.
A simple web application which allows you to log in as one of a selection of
predefined users (each with different roles and attributes), and then display
the contacts
that the given user has access to. Consisting of (in
a nutshell):
First things first, we need to configure our database. In this demo, we
streamline the process significantly by relying on a SQLite instance which
persists to local disk. With Prisma, this is very simple to configure. Check
out this section of prisma/schema.prisma
:
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
This tells Prisma that we want to use a SQLite database, and to persist it to
a relative file at ./dev.db
.
Check out the rest of the file, you’ll see some model definitions further down which Prisma will use when instantiating the database. Each model relates to a table definition, e.g.:
model User {
id String @id @default(cuid())
username String @unique
email String @unique
name String?
contacts Contact[]
department String
}
Once we’ve defined our DB provider and our data models, we tell Prisma to create out SQLite database and tables with the following:
npx prisma migrate dev --name init
You’ll see the prisma/migrations/
directory will have appeared,
and the client will be ready to talk to the DB.
For the purposes of this demo, we’ve mocked out the Identity Provider (IdP) by
simply defining some static users in code, which will be instantiated and held
in memory when the app is fired up. You can see this in the aptly named
lib/idp.ts
file:
export const users = [
{
username: "alice",
password: "supersecret",
role: "admin",
},
…
},
];
In production, you could substitute this for your own IdP, whether that be a hosted solution or a custom one.
Next.js provides a function called getServerSideProps
, which
allows you to fetch data on the server-side and pass it to the component as
props. It is used to provide statically generated HTML pages whilst still
allowing the data to be dynamic, providing a fast and efficient way to fetch
and display data in a Next.js application.
In the context of authorization, this is hugely useful. We can leverage Cerbos
and precompute access to resources before passing them to the client for
rendering. Check out getServerSideProps
in
pages/contacts.tsx
. A few things happen here: firstly, we check
for an active session; if not present, we’ll redirect the user to the login
page. Otherwise, we retrieve the user info from the session and build further
contextual data via a call to the database with Prisma:
const user = await prisma.user.findUnique({
where: { username: session.username },
});
if (!user) {
throw Error("Not found");
}
We can then use this additional context to construct a payload to pass to the Cerbos query planner:
const contactQueryPlan = await cerbos.planResources({
principal: {
id: user.id,
roles: [session.role],
attributes: {
department: user.department,
},
},
resource: {
kind: "contact",
},
action: "read",
});
And then use the handy query plan adapter to convert it into a Prisma query to retrieve all of the allowed resources for the principal:
let contacts: any[] = [];
const queryPlanResult = queryPlanToPrisma({
queryPlan: contactQueryPlan,
// map or function to change field names to match the prisma model
fieldNameMapper: {
"request.resource.attr.ownerId": "ownerId",
"request.resource.attr.department": "department",
"request.resource.attr.active": "active",
"request.resource.attr.marketingOptIn": "marketingOptIn",
},
});
Now we have our allowed resources (contacts), and the user data, we can pass these to the React component as props from the function:
return { props: { contacts, user } };
All of this computation occurs on the back end, so the client will only receive data for resources that we already know it has access to!
We use similar logic for the contact detail page, see
pages/contacts/[cid].tsx
. The difference here is that we
construct contextual data for both the principal (the user) and the resource
(the contact), and check access for that specific combination and the action
"read":
const decision = await cerbos.checkResource({
principal: {
id: user.id,
roles: [session.role],
attributes: {
department: user.department,
},
},
resource: {
kind: "contact",
id: contact.id + "",
attributes: JSON.parse(JSON.stringify(contact)),
},
actions: ["read"],
});
If you’ve pulled the code from the repo, running the app locally is as simple as running a couple of commands. But we can go a step further than that! Let’s go ahead and deploy our demo app to Vercel.
First things first, you'll need to create an account (if you haven't already) and connect your project to a git provider.
Once you've done that, we can make a couple of small adaptations to the code and use some handy tooling to enable us to host our streamlined demo on Vercel with no other hosted deployments required!
The demo currently uses SQLite. Vercel offers ephemeral compute primitives without persistent storage (see here), so deploying the app as-is won't work.
In production, it's likely that you'd separately provision your database and then point your app at it, but for this demo, we can provision one locally and then use ngrok to expose that database to the public internet! At the time of writing, they offer a free tier allowing you to proxy a single service on a randomly generated URL at any one time.
The following steps will show you how to configure a local instance of PostgreSQL and adapt the code to talk to this DB instance via ngrok.
ngrok tcp 5432
(change the port if necessary)
psql
) and create a
Postgres user and database:
CREATE USER test_user WITH PASSWORD 'password123' CREATEDB;
CREATE DATABASE cerbos;
datasource db
in prisma/schema.prisma
to
point to Postgres via an environment variable:
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
prisma/migrations/
folder prior to running the following):
npx prisma migrate dev --name init
npx prisma db seed
DATABASE_URL
to the appropriate
connection string in the format:
postgresql://{user}:{password}@{ngrok_url}:{ngrok_port}/{db_name}
. E.g. if the ngrok "forwarding" output was
tcp://4.tcp.eu.ngrok.io:12348 -> localhost:5432
, then (assuming
the parameters in the example SQL above) the connection string would be
postgresql://some_user:password1234@4.tcp.eu.ngrok.io:12348/cerbos
.
We also need access to an instance of the Cerbos PDP. Again, in production, you'd manage your Cerbos deployments separately, but for this demo we can rely on the Cerbos playground to store and serve our policies. For convenience, we've created a playground instance with the required policies which you can use here.
To point to this PDP instance, back in the Vercel project settings, set the
environment variable CERBOS_URL
to "demo-pdp.cerbos.cloud". You
can optionally set CERBOS_PLAYGROUND_INSTANCE
as well, but if you
don't, it'll default to the provided instance.
Deploy the preview to Vercel (running vercel
in the project root
will trigger this), and load it up. You should now have a Vercel hosted
deployment, using an instance of Postgres on your local machine and a public
instance of the Cerbos PDP running in the playground!
After the changes we made above, we now have an app deployed on Vercel, using a locally running database (through the magic of ngrok) and a convenient test instance of Cerbos. This is fine for prototyping, but how would we make the step to a full production system? There are (potentially) two major considerations for this:
First things first, we need to provision a database. Needless to say, there are countless methods and hosting providers upon which we can do so. It’s even quite possible that you already have your database running in your production environment.
Adapting the code to point to your production database is as simple as
updating the datasource db
section in
prisma/schema.prisma
. Prisma supports a
wide range of databases; update the provider, if necessary (you could handle this through an env var
too) and set the url to the appropriate connection string! Prisma gives
lots of help
on this.
The other missing component is a production ready instance of Cerbos. At the moment, we’re pointing to the Cerbos-provided playground instance. This is great for prototyping, but as touched on previously, not recommended for production use.
In the same vein as our database, the way you deploy Cerbos is entirely
dependent on your own infrastructure and particular needs. Cerbos can be
deployed in a
variety of ways. Once it’s available, you can point your app to it by setting the
CERBOS_URL
environment variable.
By integrating Next.js, Prisma, and Cerbos, we have shown how to build a simple and secure web application with dynamic and protected content. The combination of Next.js' server-side rendering, Prisma's flexible data-driven capabilities, and Cerbos' authorization layer provides a powerful solution for building modern web experiences.
As always, if you have any questions or feedback, head over to our Slack community and ask away!
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team
Join thousands of developers | Features and updates | 1x per month | No spam, just goodies.