Data protection in Next.js Pages Router with Protect.js and Supabase

cj-avatar
CJ Brewer

Protecting sensitive user data is more crucial than ever. As developers, we need to ensure that our applications handle sensitive information securely, especially when dealing with personal information like email addresses and other PII data. In this post, we'll explore how to implement robust data protection in the Next.js Pages Router using CipherStash Protect.js and Supabase as a datastore.

The full example repo is available in Github.

Why data protection matters

Before diving into the implementation, let's understand why data protection is essential:

  • Regulatory compliance — Many jurisdictions and compliance frameworks require proper handling of personal data (like GDPR, CCPA, and HIPAA).

  • Customer trust — Your customers expect their sensitive information to be handled securely.

  • Data breach prevention — Proper encryption helps mitigate the impact of potential data breaches.

The tech stack

Our approach in this post combines three technologies:

  • Next.js 14 — A React framework with built-in server-side rendering

  • CipherStash Protect.js — A powerful data protection library

  • Supabase — A modern, open-source Firebase alternative with PostgreSQL

Implementation overview

1. Setting up the environment

First, configure environment variables:

NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
CS_CLIENT_ID=your_client_id
CS_CLIENT_KEY=your_client_key
CS_CLIENT_ACCESS_KEY=your_access_key
CS_WORKSPACE_ID=your_workspace_id

Get your Supabase credentials from their dashboard using the "Connect" button in the navigation menu.

Generate your CipherStash credentials through the CipherStash Dashboard.

2. Data protection strategy

Our implementation follows these key principles:

  • Server-Side encryption — All encryption and decryption happens on the application server

  • Type safety — Using TypeScript and Zod for runtime type validation

  • Bulk encryption operations — Efficient handling of multiple records to ensure performance

  • Secure API routes — Protected endpoints for data manipulation

3. Key implementation details

Server-Side decryption

We use Next.js's getServerSideProps to handle fetching data from Supabase and decrypting it on the server before passing it as a prop to the client component:

export const getServerSideProps: GetServerSideProps = async () => {
  try {
    // 1. Query Supabase for all users 
    const { data, error } = await supabase.from("users").select();

    if (error) {
      throw new Error(error.message);
    }

    // 2. Parse and validate the data through our schema
    const encryptedUsers = data?.map((user) => encryptedUserSchema.parse(user));

    // 3. Use Protect.js to bulk decrypt all encrypted email fields
    const result = await protectClient.bulkDecryptModels<EncryptedUser>(
      encryptedUsers
    );

    if (result.failure) {
      throw new Error(result.failure.message);
    }

    // 4. Parse decrypted data through our schema for final validation
    const users = result.data.map((user) => decryptedUserSchema.parse(user));

    return {
      props: {
        users, // Array of users with decrypted emails, safe to use in components
      },
    };
  } catch (error) {
    console.error("Error fetching users:", error);
    return {
      props: {
        users: [], // Return empty array on error to prevent crashes
      },
    };
  }
};

Type-safe data handling

We use Zod schemas to ensure data integrity:

import { z } from 'zod'
import type { EncryptedPayload } from '@cipherstash/protect'

const encryptedUserSchema = z.object({
  id: z.number(),
  email: z.string(),
  email_encrypted: z.custom<EncryptedPayload>(),
});

const decryptedUserSchema = z.object({
  id: z.number(),
  email: z.string(),
  email_encrypted: z.string(),
});

Secure data operations

When creating new records, we use API routes to handle encryption since the Next.js 14 Pages Router doesn't support server actions.

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    // Extract email from request body
    const { email } = req.body

    if (!email) {
      return res.status(400).json({ error: 'Email is required' })
    }

    // Encrypt the email using CipherStash Protect
    const result = await protectClient.encrypt(email, {
      table: users,
      column: users.email_encrypted,
    })

    // You will always need to handle encryption failure
    if (result.failure) {
      return res.status(500).json({ error: result.failure })
    }

    // Insert the user into the database with both plain and encrypted email
    // for example purposes.
    const { data, error } = await supabase
      .from('users')
      .insert([{ email, email_encrypted: result.data }])
      .select()
      .single()

    if (error) {
      return res.status(500).json({ error })
    }

    return res.status(200).json(data)
  } catch (error) {
    console.error('Error creating user:', error)
    return res.status(500).json({ error: 'Internal server error' })
  }
}

Benefits of this approach

  • Enhanced security — Data is encrypted in use, not just at rest and in transit.

  • Developer experience — Protect.js provides a seamless user experience for handling complex cryptography operations.

  • Scalability — The solution works well for both small and large applications.

  • Maintainability — Clear separation of concerns and type safety.

It's not complicated!

Implementing data protection in a Next.js application doesn't have to be complicated. By using Protect.js with Supabase, we can create a secure, type-safe, and maintainable solution for handling sensitive data. The combination of server-side processing, type safety, and proper encryption ensures that your application meets modern security standards while remaining developer-friendly.

More info

Remember, security isn't a one-time implementation but an ongoing process. Always stay updated with the latest security best practices and regularly audit your application's security measures.

Start protecting your data

Get started by creating a free account and choosing your integration path, or get in touch to learn more.