Implement robust data security without sacrificing performance or usability
Protect.js is a TypeScript package for encrypting and decrypting data. Encryption operations happen directly in your app, and the ciphertext is stored in your database.
Every value you encrypt with Protect.js has a unique key, made possible by CipherStash ZeroKMS's blazing fast bulk key operations, and backed by a root key in AWS KMS.
The encrypted data is structured as an EQL JSON payload, and can be stored in any database that supports JSONB.
Important
Searching, sorting, and filtering on encrypted data is currently only supported when storing encrypted data in PostgreSQL. Read more about searching encrypted data.
- Features
- Installing Protect.js
- Getting started
- Working with models and objects
- Identity-aware encryption
- Supported data types
- Searchable encryption
- Logging
- CipherStash Client
- Example applications
- Builds and bundling
- Contributing
- License
For more specific documentation, refer to the docs.
Protect.js protects data in using industry-standard AES encryption. Protect.js uses ZeroKMS for bulk encryption and decryption operations. This enables every encrypted value, in every column, in every row in your database to have a unique key — without sacrificing performance.
Features:
- Bulk encryption and decryption: Protect.js uses ZeroKMS for encrypting and decrypting thousands of records at once, while using a unique key for every value.
- Single item encryption and decryption: Just looking for a way to encrypt and decrypt single values? Protect.js has you covered.
- Really fast: ZeroKMS's performance makes using millions of unique keys feasible and performant for real-world applications built with Protect.js.
- Identity-aware encryption: Lock down access to sensitive data by requiring a valid JWT to perform a decryption.
- Audit trail: Every decryption event will be logged in ZeroKMS to help you prove compliance.
- Searchable encryption: Protect.js supports searching encrypted data in PostgreSQL.
- TypeScript support: Strongly typed with TypeScript interfaces and types.
Use cases:
- Trusted data access: make sure only your end-users can access their sensitive data stored in your product.
- Meet compliance requirements faster: meet and exceed the data encryption requirements of SOC2 and ISO27001.
- Reduce the blast radius of data breaches: limit the impact of exploited vulnerabilities to only the data your end-users can decrypt.
Install the @cipherstash/protect
package with your package manager of choice:
npm install @cipherstash/protect
# or
yarn add @cipherstash/protect
# or
pnpm add @cipherstash/protect
Tip
Bun is not currently supported due to a lack of Node-API compatibility. Under the hood, Protect.js uses CipherStash Client which is written in Rust and embedded using Neon.
Lastly, install the CipherStash CLI:
-
On macOS:
brew install cipherstash/tap/stash
-
On Linux, download the binary for your platform, and put it on your
PATH
:
Important
You need to opt-out of bundling when using Protect.js.
Protect.js uses Node.js specific features and requires the use of the native Node.js require
.
When using Protect.js, you need to opt-out of bundling for tools like Webpack, esbuild, or Next.js.
Read more about building and bundling with Protect.js.
🆕 Existing app? Skip to the next step.
🌱 Clean slate? Check out the getting started tutorial.
Important
Make sure you have installed the CipherStash CLI before following these steps.
To set up all the configuration and credentials required for Protect.js:
stash setup
If you haven't already signed up for a CipherStash account, this will prompt you to do so along the way.
At the end of stash setup
, you will have two files in your project:
cipherstash.toml
which contains the configuration for Protect.jscipherstash.secret.toml
: which contains the credentials for Protect.js
Warning
Don't commit cipherstash.secret.toml
to git; it contains sensitive credentials.
The stash setup
command will attempt to append to your .gitignore
file with the cipherstash.secret.toml
file.
Read more about configuration via TOML file or environment variables.
The following is the basic file structure of the project.
In the src/protect/
directory, we have the table definition in schema.ts
and the protect client in index.ts
.
📦 <project root>
├ 📂 src
│ ├ 📂 protect
│ │ ├ 📜 index.ts
│ │ └ 📜 schema.ts
│ └ 📜 index.ts
├ 📜 .env
├ 📜 cipherstash.toml
├ 📜 cipherstash.secret.toml
├ 📜 package.json
└ 📜 tsconfig.json
Protect.js uses a schema to define the tables and columns that you want to encrypt and decrypt.
Define your tables and columns by adding this to src/protect/schema.ts
:
import { csTable, csColumn } from "@cipherstash/protect";
export const users = csTable("users", {
email: csColumn("email"),
});
export const orders = csTable("orders", {
address: csColumn("address"),
});
Searchable encryption:
If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in src/protect/schema.ts
:
import { csTable, csColumn } from "@cipherstash/protect";
export const users = csTable("users", {
email: csColumn("email").freeTextSearch().equality().orderAndRange(),
});
export const orders = csTable("orders", {
address: csColumn("address"),
});
Read more about defining your schema.
To import the protect
function and initialize a client with your defined schema, add the following to src/protect/index.ts
:
import { protect } from "@cipherstash/protect";
import { users, orders } from "./schema";
// Pass all your tables to the protect function to initialize the client
export const protectClient = await protect(users, orders);
The protect
function requires at least one csTable
be provided.
Protect.js provides the encrypt
function on protectClient
to encrypt data.
encrypt
takes a plaintext string, and an object with the table and column as parameters.
To start encrypting data, add the following to src/index.ts
:
import { users } from "./protect/schema";
import { protectClient } from "./protect";
const encryptResult = await protectClient.encrypt("[email protected]", {
column: users.email,
table: users,
});
if (encryptResult.failure) {
// Handle the failure
console.log(
"error when encrypting:",
encryptResult.failure.type,
encryptResult.failure.message
);
}
const ciphertext = encryptResult.data;
console.log("ciphertext:", ciphertext);
The encrypt
function will return a Result
object with either a data
key, or a failure
key.
The encryptResult
will return one of the following:
// Success
{
data: {
c: 'mBbKmsMMkbKBSN}s1THy_NfQN892!dercyd0s...'
}
}
// Failure
{
failure: {
type: 'EncryptionError',
message: 'A message about the error'
}
}
Protect.js provides the decrypt
function on protectClient
to decrypt data.
decrypt
takes an encrypted data object as a parameter.
To start decrypting data, add the following to src/index.ts
:
import { protectClient } from "./protect";
const decryptResult = await protectClient.decrypt(ciphertext);
if (decryptResult.failure) {
// Handle the failure
console.log(
"error when decrypting:",
decryptResult.failure.type,
decryptResult.failure.message
);
}
const plaintext = decryptResult.data;
console.log("plaintext:", plaintext);
The decrypt
function returns a Result
object with either a data
key, or a failure
key.
The decryptResult
will return one of the following:
// Success
{
data: '[email protected]'
}
// Failure
{
failure: {
type: 'DecryptionError',
message: 'A message about the error'
}
}
Protect.js provides model-level encryption methods that make it easy to encrypt and decrypt entire objects. These methods automatically handle the encryption of fields defined in your schema.
If you are working with a large data set, the model operations are significantly faster than encrypting and decrypting individual objects as they are able to perform bulk operations.
Tip
CipherStash ZeroKMS is optimized for bulk operations.
All the model operations are able to take advantage of this performance for real-world use cases by only making a single call to ZeroKMS regardless of the number of objects you are encrypting or decrypting while still using a unique key for each record.
Use the encryptModel
method to encrypt a model's fields that are defined in your schema:
import { protectClient } from "./protect";
import { users } from "./protect/schema";
// Your model with plaintext values
const user = {
id: "1",
email: "[email protected]",
address: "123 Main St",
createdAt: new Date("2024-01-01"),
};
const encryptedResult = await protectClient.encryptModel(user, users);
if (encryptedResult.failure) {
// Handle the failure
console.log(
"error when encrypting:",
encryptedResult.failure.type,
encryptedResult.failure.message
);
}
const encryptedUser = encryptedResult.data;
console.log("encrypted user:", encryptedUser);
The encryptModel
function will only encrypt fields that are defined in your schema.
Other fields (like id
and createdAt
in the example above) will remain unchanged.
Protect.js provides strong TypeScript support for model operations. You can specify your model's type to ensure end-to-end type safety:
import { protectClient } from "./protect";
import { users } from "./protect/schema";
// Define your model type
type User = {
id: string;
email: string | null;
address: string | null;
createdAt: Date;
updatedAt: Date;
metadata?: {
preferences?: {
notifications: boolean;
theme: string;
};
};
};
// The encryptModel method will ensure type safety
const encryptedResult = await protectClient.encryptModel<User>(user, users);
if (encryptedResult.failure) {
// Handle the failure
}
const encryptedUser = encryptedResult.data;
// TypeScript knows that encryptedUser matches the User type structure
// but with encrypted fields for those defined in the schema
// Decryption maintains type safety
const decryptedResult = await protectClient.decryptModel<User>(encryptedUser);
if (decryptedResult.failure) {
// Handle the failure
}
const decryptedUser = decryptedResult.data;
// decryptedUser is fully typed as User
// Bulk operations also support type safety
const bulkEncryptedResult = await protectClient.bulkEncryptModels<User>(
userModels,
users
);
const bulkDecryptedResult = await protectClient.bulkDecryptModels<User>(
bulkEncryptedResult.data
);
The type system ensures that:
- Input models match your defined type structure
- Only fields defined in your schema are encrypted
- Encrypted and decrypted results maintain the correct type structure
- Optional and nullable fields are properly handled
- Nested object structures are preserved
- Additional properties not defined in the schema remain unchanged
This type safety helps catch potential issues at compile time and provides better IDE support with autocompletion and type hints.
Tip
When using TypeScript with an ORM, you can reuse your ORM's model types directly with Protect.js's model operations.
Example with Drizzle infered types:
import { protectClient } from "./protect";
import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core";
import { csTable, csColumn } from "@cipherstash/protect";
const protectUsers = csTable("users", {
email: csColumn("email"),
});
const users = pgTable("users", {
id: serial("id").primaryKey(),
email: jsonb("email").notNull(),
});
type User = InferSelectModel<typeof users>;
const user = {
id: "1",
email: "[email protected]",
};
// Drizzle User type works directly with model operations
const encryptedResult = await protectClient.encryptModel<User>(
user,
protectUsers
);
Use the decryptModel
method to decrypt a model's encrypted fields:
import { protectClient } from "./protect";
const decryptedResult = await protectClient.decryptModel(encryptedUser);
if (decryptedResult.failure) {
// Handle the failure
console.log(
"error when decrypting:",
decryptedResult.failure.type,
decryptedResult.failure.message
);
}
const decryptedUser = decryptedResult.data;
console.log("decrypted user:", decryptedUser);
For better performance when working with multiple models, use the bulkEncryptModels
and bulkDecryptModels
methods:
import { protectClient } from "./protect";
import { users } from "./protect/schema";
// Array of models with plaintext values
const userModels = [
{
id: "1",
email: "[email protected]",
address: "123 Main St",
},
{
id: "2",
email: "[email protected]",
address: "456 Oak Ave",
},
];
// Encrypt multiple models at once
const encryptedResult = await protectClient.bulkEncryptModels(
userModels,
users
);
if (encryptedResult.failure) {
// Handle the failure
}
const encryptedUsers = encryptedResult.data;
// Decrypt multiple models at once
const decryptedResult = await protectClient.bulkDecryptModels(encryptedUsers);
if (decryptedResult.failure) {
// Handle the failure
}
const decryptedUsers = decryptedResult.data;
The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object. They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema.
Encrypted data can be stored in any database that supports JSONB, noting that searchable encryption is only supported in PostgreSQL at the moment.
To store the encrypted data, specify the column type as jsonb
.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email jsonb NOT NULL,
);
To enable searchable encryption in PostgreSQL, install the EQL custom types and functions.
-
Download the latest EQL install script:
curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql
-
Run this command to install the custom types and functions:
psql -f cipherstash-encrypt.sql
EQL is now installed in your database and you can enable searchable encryption by adding the cs_encrypted_v1
type to a column.
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email cs_encrypted_v1
);
Important
Right now identity-aware encryption is only supported if you are using Clerk as your identity provider. Read more about lock contexts with Clerk and Next.js.
Protect.js can add an additional layer of protection to your data by requiring a valid JWT to perform a decryption.
This ensures that only the user who encrypted data is able to decrypt it.
Protect.js does this through a mechanism called a lock context.
Lock contexts ensure that only specific users can access sensitive data.
Caution
You must use the same lock context to encrypt and decrypt data. If you use different lock contexts, you will be unable to decrypt the data.
To use a lock context, initialize a LockContext
object with the identity claims.
import { LockContext } from "@cipherstash/protect/identify";
// protectClient from the previous steps
const lc = new LockContext();
Note
When initializing a LockContext
, the default context is set to use the sub
Identity Claim.
A lock context needs to be locked to a user.
To identify the user, call the identify
method on the lock context object, and pass a valid JWT from a user's session:
const identifyResult = await lc.identify(jwt);
// The identify method returns the same Result pattern as the encrypt and decrypt methods.
if (identifyResult.failure) {
// Hanlde the failure
}
const lockContext = identifyResult.data;
To encrypt data with a lock context, call the optional withLockContext
method on the encrypt
function and pass the lock context object as a parameter:
import { protectClient } from "./protect";
import { users } from "./protect/schema";
const encryptResult = await protectClient
.encrypt("plaintext", {
table: users,
column: users.email,
})
.withLockContext(lockContext);
if (encryptResult.failure) {
// Handle the failure
}
const ciphertext = encryptResult.data;
To decrypt data with a lock context, call the optional withLockContext
method on the decrypt
function and pass the lock context object as a parameter:
import { protectClient } from "./protect";
const decryptResult = await protectClient
.decrypt(ciphertext)
.withLockContext(lockContext);
if (decryptResult.failure) {
// Handle the failure
}
const plaintext = decryptResult.data;
All model operations support lock contexts for identity-aware encryption:
import { protectClient } from "./protect";
import { users } from "./protect/schema";
const myUsers = [
{
id: "1",
email: "[email protected]",
address: "123 Main St",
createdAt: new Date("2024-01-01"),
},
{
id: "2",
email: "[email protected]",
address: "456 Oak Ave",
},
];
// Encrypt a model with lock context
const encryptedResult = await protectClient
.encryptModel(myUsers[0], users)
.withLockContext(lockContext);
if (encryptedResult.failure) {
// Handle the failure
}
// Decrypt a model with lock context
const decryptedResult = await protectClient
.decryptModel(encryptedResult.data)
.withLockContext(lockContext);
// Bulk operations also support lock contexts
const bulkEncryptedResult = await protectClient
.bulkEncryptModels(myUsers, users)
.withLockContext(lockContext);
const bulkDecryptedResult = await protectClient
.bulkDecryptModels(bulkEncryptedResult.data)
.withLockContext(lockContext);
Protect.js currently supports encrypting and decrypting text. Other data types like booleans, dates, ints, floats, and JSON are well-supported in other CipherStash products, and will be coming to Protect.js soon.
Until support for other data types are available, you can express interest in this feature by adding a 👍 on this GitHub Issue.
Read more about searching encrypted data in the docs.
Important
@cipherstash/protect
will NEVER log plaintext data.
This is by design to prevent sensitive data from leaking into logs.
@cipherstash/protect
and @cipherstash/nextjs
will log to the console with a log level of info
by default.
To enable the logger, configure the following environment variable:
PROTECT_LOG_LEVEL=debug # Enable debug logging
PROTECT_LOG_LEVEL=info # Enable info logging
PROTECT_LOG_LEVEL=error # Enable error logging
Protect.js is built on top of the CipherStash Client Rust SDK which is embedded with the @cipherstash/protect-ffi
package.
The @cipherstash/protect-ffi
source code is available on GitHub.
Read more about configuring the CipherStash Client in the configuration docs.
Looking for examples of how to use Protect.js? Check out the example applications:
- Basic example demonstrates how to perform encryption operations
- Drizzle example demonstrates how to use Protect.js with an ORM
- Next.js and lock contexts example using Clerk demonstrates how to protect data with identity-aware encryption
@cipherstash/protect
can be used with most ORMs.
If you're interested in using @cipherstash/protect
with a specific ORM, please create an issue.
@cipherstash/protect
is a native Node.js module, and relies on native Node.js require
to load the package.
Here are a few resources to help based on your tool set:
Please read the contribution guide.
Protect.js is MIT licensed.