# Data protection in Next.js Pages Router with CipherStash Encryption and Supabase

*Published on 2025-05-15T00:00:00.000Z*

*By CJ Brewer*

As developers, we need to make sure that our applications handle sensitive information securely, especially when dealing with personal information like email addresses and other PII data.

## Content

{% callout type="important" title="Use Stack instead" %}
Stack supersedes Protect.js. For new Next.js + Supabase projects, read the current Stack-based guide instead: [Adding CipherStash Stack to a Next.js + Supabase app](/blog/adding-cipherstash-stack-to-nextjs-supabase) — covers both Pages Router and App Router with the API-compatible `encryptedSupabase` wrapper. This post stays up for reference.
{% /callout %}


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](https://nextjs.org/docs/pages) using [CipherStash Encryption](https://github.com/cipherstash/stack) and [Supabase](https://supabase.com) as a datastore.

The full example repo is available [in Github](https://github.com/cipherstash/protect-example-nextjs-14-supabase?tab=readme-ov-file#implementation-requirements).

## 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 Encryption**: 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:

```env
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](https://dashboard.cipherstash.com/sign-in).

### 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](https://zod.dev/) 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:

```typescript
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 CipherStash Encryption to bulk decrypt all encrypted email fields
    const result = await encryptionClient.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](https://zod.dev/) schemas to ensure data integrity:

```typescript
import { z } from 'zod'
import type { EncryptedPayload } from '@cipherstash/stack'

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](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).

```typescript
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 Encryption
    const result = await encryptionClient.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**: CipherStash Encryption 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 CipherStash Encryption 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

- [Full example repo](https://github.com/cipherstash/protect-example-nextjs-14-supabase?tab=readme-ov-file#implementation-requirements)
- [CipherStash Encryption docs](https://github.com/cipherstash/stack)
- [Next.js docs](https://nextjs.org/docs)
- [Supabase docs](https://supabase.com/docs)

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.

## Related blog posts

- [Encryption in use: 3 ways to protect sensitive data in Typescript backends](https://cipherstash.com/blog/encryption-in-use-3-ways-to-protect-sensitive-data-in-typescript-backends.md) — When building a JavaScript or TypeScript application, few concerns weigh more heavily than the secure handling of sensitive data. Whether you're dealing with personal user information, health records, or simply wanting to follow best practices, implementing data protection is critical.
- [Adding CipherStash Encryption to a Next.js app that uses the Supabase SDK](https://cipherstash.com/blog/add-protectjs-to-nextjs-app-with-supabase-sdk.md) — Let's look at how to add CipherStash Encryption to a Next.js app that uses the Supabase SDK and server actions to read and insert encrypted data into a Supabase Postgres instance.
- [Introducing Protect.js](https://cipherstash.com/blog/introducing-protectjs.md) — Protecting data that’s sensitive (such as personal health or financial information) is crucial to building applications that users trust. But getting access control right is a tricky business. Complex requirements and a plethora of tools, as well as performance considerations, can kill dev team productivity.

