Neon is now SOC 2 Type 2 compliant 🚀Read more
Community

How to Build a Secure Project Management Platform with Next.js, Clerk, and Neon

Learn a security-first approach to building web applications by building a secure project management platform with Next.js

Post image

This article was first published in the Clerk blog.

Around 30,000 websites and applications are hacked every day*, and the developer is often to blame.

The vast majority of breaches occur due to misconfiguration rather than an actual vulnerability. This could be due to exposed database credentials, unprotected API routes, or data operations without the proper authorization checks just to name a few. It’s important to ensure that your application is configured in a way that prevents attackers from gaining unauthorized access to user data.

In this article, you’ll learn how to build a project management web application while considering security best practices throughout. We’ll cover the process of building Kozi – a collaborative project and knowledge management tool.

Throughout the series, the following features will be implemented:

  • Create organizations to invite others to manage projects as a team.
  • A rich, collaborative text editor for project and task notes.
  • A system to comment on projects, tasks, and notes.
  • Automatic RAG functionality for all notes and uploaded files.
  • Invite users from outside your organization to collaborate on individual tasks.
  • Be notified when events occur on tasks you subscribe to, or you are mentioned in comments or notes.

What makes this a “secure” project management system?

Data security is considered throughout this guide by using the following techniques:

Clerk and the Next.js middleware

Clerk is a user management platform designed to get authentication into your application as quick as possible by providing a complete suite of user management tools as well as drop-in UI components. Behind the scenes, Clerk creates fast expiring tokens upon user sign-in that are sent to your server with each request, where Clerk also verifies the identify of the user.

Clerk integrates with Next.js middleware to ensure every request to the application is evaluated before it reaches its destination. In the section where the middleware is configured, we instruct the middleware to protect any route starting with /app so that only authenticated users may access them. This means that before any functions are executed (on the client or server), the user will need to be authenticated.

Server actions

In this project, server actions are the primary method of interacting with the data in the database. Direct access to the database should always happen on the server and NEVER on the client where tech-savvy users can gain access to the database credentials. Since all functions that access the database are built with server actions, they do not execute client-side.

It’s important to note that calling these server actions should only ever be performed from protected routes. When a Next.js client component executes a server action, an HTTP POST request of form data is submitted to the current path with a unique identifier of the action for Next.js to route the data internally.

This means that calling a server function from an anonymous route might result in anonymous users getting access to the data. This potential vulnerability is addressed in the next section.

Database requests

Protecting access to the functions is only one consideration. Each request will have an accompanying user identifier which can be used to determine the user making that request. This identifier is stored alongside the records the user creates, allowing each request for data to ONLY return the data associated with that user.

When making data modifications, the requesting user ID is cross-referenced with the records being modified or deleted so that one user cannot affect another user’s data.

The combination of protecting access to the routes, being mindful of calling server actions, and cross-referencing database queries with the user making the request ensures that the data within the application is secure and only accessible to those who have access to it.

How to follow along

Kozi is an open-source project, with each article in the series having corresponding start and end branches. This makes it easy to jump in at any point to get hands-on experience with the concepts outlined in each piece, as well as a point of reference if you simply want to see the completed code. Here are links to the specific branches:

You should have a basic understanding of Next.js and React as well.

Launching the project

Once the branch above is cloned, open the project in your editor or terminal and run the following command to start up the application:

Open your browser and navigate to the URL displayed in the terminal to access Kozi. At the bottom right of the screen, you should see Clerk is running in keyless mode. Click the button to claim your keys and associate this instance to your Clerk account. If you don’t have an account, you’ll be prompted to create one.

Post image

You are now ready to start building out the core functionality of Kozi!

Setting up the database

To store structured data, you’ll be using a serverless instance of Postgress provided by Neon. Start by creating a Free Neon account if you don’t have one. Create a new database and copy the connection string as shown below.

Post image

Create a new file in your local project named .env.local and paste the following snippet, replacing the placeholder for your specific Neon database connection string.

Configuring Prisma

Prisma is used as the ORM to access and manipulate data in the database, as well as apply schema changes to the database as the data needs are updated. Open the project in your IDE and start by creating the schema file at prisma/schema.prisma. Paste in the following code:

note

We’re using the owner_id column instead of user_id since this application will be updated to support teams and organizations in a future entry.

Next, create the src/lib/db.ts file and paste in the following code which will be used throughout the application to create a connection to the database:

To sync the schema changes to Neon, run the following command in the terminal:

If you open the database in the Neon console and navigate to the Tables menu item, you should see the projectsand tasks tables shown.

Post image

Finally, since it is not best practice to use the Prisma client in any client-side components, you’ll want a file to store interfaces so that TypeScript can recognize the structure of your objects when passing them between components.

Create the src/app/app/models.ts file and paste in the following:

Configure /app as a protected route with Clerk

Clerk’s middleware uses a helper function called createRouteMatcher that lets you define a list of routes to protect. This includes any pages, server actions, or API handlers stored in the matching folders of the project.

All of the core functionality of the application will be stored in the /app route, so update src/middleware.ts to use the createRouteMatcher to protect everything in that folder:

The /app route will use a different layout from the landing page, which will contain a collapsible sidebar that contains the <UserButton /> (a Clerk UI component that lets users manage their profile and sign out), an inbox for tasks, and a list of projects that tasks can be created in.

Start by creating the src/app/app/components/Sidebar.tsx file to render the elements of the sidebar:

Now create src/app/app/layout.tsx to render the sidebar with the pages in the /app route:

Next, create src/app/app/page.tsx which is just a simple page that renders some text to make sure the /approute works as expected:

Open the application in your browser and test out the changes by navigating to the /app which should automatically redirect you to the /sign-in route where you can create an account and make sure /app only works when authenticated.

Working with tasks

At the core of every project is a list of tasks, so now we’ll configure the ability to create and work with tasks in the default Inbox list. Several components will be used to provide the following application structure. The following image shows how these components will be used:

Post image

These are all client components so they will need corresponding server actions so they can interact with the database securely. Create the src/app/app/actions.ts file and paste in the following code:

We’re going to start with the <CreateTaskInput /> component which renders the field where users can create tasks. Create the src/app/app/components/CreateTaskInput.tsx file and paste in the following:

Next, we’ll move on to <TaskCard />, which will display the name of the task and allow users to toggle it using a checkbox, as is standard in task-centric applications. Create the src/app/app/components/TaskCard.tsx file and paste in the following:

Finally, create the <TaskList /> component to render the list of tasks and the input to create new ones. Create the src/app/app/components/TaskList.tsx file and paste in the following:

With all of our components created, update the src/app/app/page.tsx to match the following code which uses the components created above, as well as queries the database for all tasks on load:

If you access the application again, you can now create tasks in your inbox and complete them.

Editing and deleting tasks

Now that you can create tasks, the next step is to set up a modal so clicking the task (outside of the checkbox) will display the modal and allow you to change the name of the task and set a description if needed.

As a design decision, this modal does not include a save button but rather debounces any edits for 1 second to create an experience where users can quickly save values and avoid another click. The modal will also create a menu in the header which allows you to delete the task.

Start by appending the following code to src/app/app/actions.ts:

Next, create the src/app/app/components/EditTaskModal.tsx and paste in the following:

Finally, update src/app/app/TaskCard.tsx to include the EditTaskModal component and handle user click events:

Now you can click anywhere outside of the checkbox of a task to open the modal to edit the task name and description or delete the task from the database.

Working with projects

Users of Kozi can create projects to organize their tasks into categorized lists. Projects will be listed in the sidebar in their own section from the Inbox. When selected, the user will navigate to the /app/projects/[_id] route to see the tasks for that project. To start implementing this, update src/app/app/actions.ts to match the following:

Next, you’ll need to create the page to render the tasks for a given project. Create src/app/app/projects/[_id]/page.tsx and paste in the following:

Notice in the TaskList component that we’ve added projectId to the list of props. This is so that the currently active project ID can be passed to CreateTaskInput so that when a task is created, it knows what project to associate it with. Let’s update those two components now.

Modify app/src/src/components/CreateTaskInput.tsx to match the following:

Next, update the TaskCard component to pass the name of the selected project through to the EditTaskModal to provide a quick reference to what project the task is part of.

Edit src/app/app/components/TaskCard.tsx to match the following:

Now update src/app/app/components/TaskList.tsx to include the projectId prop and pass it to CreateTaskInput:

In order to access project data in real time from multiple client-side components, we’re going to use a Zustand store to keep things synchronized throughout the application. Using a store will allow projects to be edited and deleted without having to refresh the page. This will become more evident in the subsequent sections.

Create src/lib/store.ts and paste in the following:

The projects will be listed in the sidebar, alongside a button to create new projects as needed. Each element in the list will be its own component. Create src/app/app/components/ProjectLink.tsx and paste in the following:

Let’s create a component that will live in the sidebar that opens a modal to create a new project. Create the src/app/app/components/CreateProjectButton.tsx file and paste in the following:

Finally, you’ll update the sidebar to query the list of projects and populate the store when the component renders. Update src/app/app/components/Sidebar.tsx to match the following:

You can now add projects from the sidebar and add tasks to those projects.

Editing and deleting projects

Following the same design approach as earlier, we’ll now update the project page so that users can simply click the name of a project to edit it. We’ll also debounce the save so there is no need to manually click a save button. Because a Zustand store is being used, updating the name of the project in the store will automatically cause the new name to be displayed in the sidebar without having to refresh the page.

Start by appending the following server actions to src/app/app/actions.ts:

Since the project name is rendered in the <TaskList /> component, update src/app/app/components/TaskList.tsx to match the following:

To delete projects, we’ll use the same approach as we did with tasks by rendering a dropdown menu with an option to delete the project. Instead of in a modal though, we’ll add it to the <ProjectLink /> component so that when the user hovers over a project in the sidebar, the menu icon will be displayed as a clickable button.

Update src/app/app/components/ProjectLink.tsx to match the following code:

You can now update the names of projects and delete them as needed. Deleting a project will also delete any associated tasks with that project.

Conclusion

When building any application, security should always be something considered early on in the process. By considering the principles laid out in this article, you can build a secure system with ease using Clerk and properly structuring the code that accesses your database.

In the next article of the series, we’ll explore how you can securely access the data within your Neon database from the front end using Row Level Security using Clerk.

*Source: How Many Cyber Attacks Per Day: The Latest Stats and Impact in 2025


This article was first published in the Clerk blog.