Adding Protect.js to a Next.js app that uses the Supabase SDK

cj-avatar
CJ Brewer

Let's look at how to add Protect.js to a Next.js app that uses the Supabase SDK and server actions to read and insert encrypted data into a Supabase Postgres instance.

This is the first installment of the Protect.js tutorial blog series. In these posts I’ll be covering how you can integrate Protect.js into popular frameworks and tools, rather than just focusing on business value. I want to show how convenient it is to add Protect.js to your tech stack and gain all the security benefits of CipherStash Encryption!

This post will cover how to add Protect.js to a Next.js app that uses the Supabase SDK and server actions to read and insert encrypted data into a Supabase Postgres instance.

For the security-minded folks, it’s worth calling out some points about CipherStash encryption before continuing:

  • CipherStash encryption is backed by ZeroKMS which will use a unique encryption key for every single record, also referred to as field-level encryption

    This enables cryptographically proven audit trails of decryption events. In simpler terms, we can provide you an audit log of every data access event to help meet compliance and stringent customer requirements.

  • Protect.js encrypts data using AES-256-GCM-SIV with a unique key per value

  • ZeroKMS supports bulk operations, and is up to 14x faster than AWS KMS

The following tutorial assumes a few things:

  • You have an existing Next.js app

  • You have an existing Supabase instance

Step 1: Add Protect.js

Install the Protect.js npm package:

npm install @cipherstash/protect

Under the hood, Protect.js uses the Neon framework to create JavaScript Rust bindings to the CipherStash SDK which handles the crypto functions and communication with ZeroKMS. Given this, you'll need to use the native Node.js require which is achieved by opting out of server bundling.

In your next.config.json file add the following directive:

// Referenced in the Next.js docs: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages
const nextConfig = {
  serverExternalPackages: ['@cipherstash/protect'],
}

module.exports = nextConfig

Step 2: Install the CipherStash CLI

The CLI is used to bootstrap your CipherStash environment so go ahead and install it:

brew install cipherstash/tap/stash

If you're using Linux, follow the instructions in the docs.

Step 3: Bootstrap your environment

Now that Stash CLI is installed, run the setup command and follow the prompts to either login/signup, create a new key set, and store the settings in the TOML files.

stash setup

When it's time to go to production, you can run this process again and output the settings as environment variables which you can then store securely 🚀.

Before continuing to the next step, make sure that you have the following files in the root of your project directory.

  • cipherstash.toml

  • cipherstash.secret.toml (the CLI tool will automatically append this file name to your .gitignore file to ensure it is not checked into source)

Step 4: Init the protect client

Protect.js uses a domain-specific schema definition to configure the state of CipherStash Encryption. This state will determine how to encrypt the data, whether you need encrypted indexes or not. Encrypted indexes is how CipherStash encryption enables the use of Searchable Encryption, which you can read more about it in the docs.

Create a protect directory in either the root of your app, or in the src directory if you have that configured for your project. Also create the following files inside the protect directory:

  • index.ts

  • schema.ts

Open the schema.ts file and define a schema using the exported helper functions:

import { csTable, csColumn } from '@cipherstash/protect'

// The csTable function takes two arguments:
// - the corresponding table name in your Supabase database
// - An object, where the keys are the columns that store encrypted data
export const users = csTable('users', {
	// The csColumn function takes a single argument:
	// - the corresponding column name in your Supabase table
	email: csColumn('email'),
})

Now that a schema is defined, open the protect/index.ts file and initialize the protect client:

import { protect } from '@cipherstash/protect'
import { users } from './schema'

// The protect function takes N number of arguments,
// where each argument is a Protect.js schema.
export const protectClient = await protect(users)

These patterns might look familiar and that’s because when we were creating the Protect.js interface we didn’t want to re-invent the wheel, but rather use existing patterns leveraged in other libraries (e.g. Drizzle ORM was a major influence 🙂).

Step 5: Update your Supabase schema

Encrypted data is stored using the data type jsonb so update the column in the Supabase instance.

In most cases, this tutorial is meant to be run through with a playground environment where the environment can be modified easily and without side effects. If you're looking to modify a production instance, you'll need to consider a migration plan. If you’d like to chat about what that might look like, our solutions team would love to help out — multiple contact options are available here.

Step 6: Encrypt data

You can use the encrypt and bulkEncrypt functions in any server side component (which includes Vercel Edge Functions or Cloudflare workers). Each operation in the Protect.js library leverages a Result pattern which is outlined in the code example below:

import { protectClient } from '@/protect'
import { users } from '@/protect/schema'

// The encrypt function takes two arguments:
// - the plaintext data
// - an object defining the table and column that the data will be stored into
const result = protectClient.encrypt('plaintext', {
	table: users,
	column: users.email,
})

// The result will alway return either a failure OR data key but never both
if (result.failure) {
	// handle the failure explicitly
}

const encryptedPayload = result.data

Step 7: Insert the data using the Supabase SDK

The Supabase SDK for JavaScript makes it really easy to insert data directly into your Supabase instance. Given you’ve already configured the SDK, you would insert the data like this:

const { data, error } = await supabase
    .from('users')
    .insert([{ email: encryptedPayload }])
    .single()

If you want to learn more about the types for the encrypted payload data, read more about Encrypted Query Language format in the docs.

Typically you’d handle the inserting of data in a Next.js server action or through your API!

Conclusion

Now you’ve seen how easy it is to integrate CipherStash encryption into your Next.js project, using Protect.js and storing the encrypted data in a Supabase Postgres instance! As you try it out, keep in mind all of the security enhancements you’re getting with field-level encryption, plus those handy audit logs of every data access event.

That’s it for this first installment of the Protect.js tutorial series! 🙂 Stay tuned for more tutorials where we’ll dive into advanced features, like searchable encryption, and continue to provide examples for other popular databases and frameworks. Cheers!

If you’re interested in giving Protect.js a try, install the npm package and create a CipherStash account!

Start protecting your data

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