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

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.