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

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!