Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/searchable-json-query-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cipherstash/protect": major
"@cipherstash/schema": major
---

Add searchable JSON query API with path and containment query support
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,10 @@ mise.local.toml
cipherstash.toml
cipherstash.secret.toml
sql/cipherstash-*.sql

# work files
.claude/
.serena/
.work/
**/.work/
PR_REVIEW.md
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ If these variables are missing, tests that require live encryption will fail or
- `encryptModel(model, table)` / `decryptModel(model)`
- `bulkEncrypt(plaintexts[], { table, column })` / `bulkDecrypt(encrypted[])`
- `bulkEncryptModels(models[], table)` / `bulkDecryptModels(models[])`
- `createSearchTerms(terms)` for searchable queries
- `encryptQuery(terms)` for searchable queries (note: `createSearchTerms` is deprecated, use `encryptQuery` instead)
- **Identity-aware encryption**: Use `LockContext` from `@cipherstash/protect/identify` and chain `.withLockContext()` on operations. Same context must be used for both encrypt and decrypt.

## Critical Gotchas (read before coding)
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The documentation for Protect.js is organized into the following sections:
## Concepts

- [Searchable encryption](./concepts/searchable-encryption.md)
- [Searchable JSON](./reference/schema.md#searchable-json) - Query encrypted JSON documents

## Reference

Expand Down
7 changes: 4 additions & 3 deletions docs/concepts/aws-kms-vs-cipherstash-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,12 @@ const encryptResult = await protectClient.encrypt(
);

// Create search terms and query directly in PostgreSQL
const searchTerms = await protectClient.createSearchTerms({
terms: ['secret'],
const searchTerms = await protectClient.encryptQuery([{
value: 'secret',
column: users.email,
table: users,
});
queryType: 'freeTextSearch',
}]);

// Use with your ORM (Drizzle integration included)
```
Expand Down
11 changes: 6 additions & 5 deletions docs/concepts/searchable-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@ CipherStash uses [EQL](https://github.com/cipherstash/encrypt-query-language) to
// 1) Encrypt the search term
const searchTerm = 'alice.johnson@example.com'

const encryptedParam = await protectClient.createSearchTerms([{
const encryptedParam = await protectClient.encryptQuery([{
value: searchTerm,
table: protectedUsers, // Reference to the Protect table schema
column: protectedUsers.email, // Your Protect column definition
queryType: 'equality', // Use 'equality' for exact match queries
}])

if (encryptedParam.failure) {
// Handle the failure
throw new Error(encryptedParam.failure.message)
}

// 2) Build an equality query noting that EQL must be installed in order for the operation to work successfully
Expand All @@ -86,10 +88,9 @@ const equalitySQL = `
WHERE email = $1
`

// 3) Execute the query, passing in the Postgres column name
// and the encrypted search term as the second parameter
// 3) Execute the query, passing in the encrypted search term
// (client is an arbitrary Postgres client)
const result = await client.query(equalitySQL, [ protectedUser.email.getName(), encryptedParam.data ])
const result = await client.query(equalitySQL, [encryptedParam.data[0]])
```

Using the above approach, Protect.js is generating the EQL payloads and which means you never have to drop down to writing complex SQL queries.
Expand Down Expand Up @@ -132,7 +133,7 @@ With searchable encryption, you can:
With searchable encryption:

- Data can be encrypted, stored, and searched in your existing PostgreSQL database.
- Encrypted data can be searched using equality, free text search, and range queries.
- Encrypted data can be searched using equality, free text search, range queries, and JSON path/containment queries.
- Data remains encrypted, and will be decrypted using the Protect.js library in your application.
- Queries are blazing fast, and won't slow down your application experience.
- Every decryption event is logged, giving you an audit trail of data access events.
Expand Down
8 changes: 8 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ CREATE TABLE users (
);
```

## Next steps

Now that you have the basics working, explore these advanced features:

- **[Searchable Encryption](./reference/searchable-encryption-postgres.md)** - Learn how to search encrypted data using `encryptQuery()` with PostgreSQL and EQL
- **[Model Operations](./reference/model-operations.md)** - Encrypt and decrypt entire objects with bulk operations
- **[Schema Configuration](./reference/schema.md)** - Configure indexes for equality, free text search, range queries, and JSON search

---

### Didn't find what you wanted?
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/model-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ For better performance when working with multiple models, use these bulk encrypt
### Bulk encryption

```typescript
const users = [
const usersList = [
{
id: "1",
email: "user1@example.com",
Expand All @@ -88,7 +88,7 @@ const users = [
},
];

const encryptedResult = await protectClient.bulkEncryptModels(users, users);
const encryptedResult = await protectClient.bulkEncryptModels(usersList, usersSchema);

if (encryptedResult.failure) {
console.error("Bulk encryption failed:", encryptedResult.failure.message);
Expand Down
34 changes: 29 additions & 5 deletions docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,29 @@ export const protectedUsers = csTable("users", {
});
```

### Searchable JSON

To enable searching within JSON columns, use the `searchableJson()` method. This automatically sets the column data type to `json` and configures the necessary indexes for path and containment queries.

```ts
import { csTable, csColumn } from "@cipherstash/protect";

export const protectedUsers = csTable("users", {
metadata: csColumn("metadata").searchableJson(),
});
```

> [!WARNING]
> `searchableJson()` is mutually exclusive with other index types (`equality()`, `freeTextSearch()`, `orderAndRange()`) on the same column. Combining them will result in runtime errors. This is enforced by the encryption backend, not at the TypeScript type level.


### Nested objects

Protect.js supports nested objects in your schema, allowing you to encrypt **but not search on** nested properties. You can define nested objects up to 3 levels deep.
Protect.js supports nested objects in your schema, allowing you to encrypt nested properties. You can define nested objects up to 3 levels deep using `csValue`. For **searchable** JSON data, use `.searchableJson()` on a JSON column instead.

> [!TIP]
> If you need to search within JSON data, use `.searchableJson()` on the column instead of nested `csValue` definitions. See [Searchable JSON](#searchable-json) above.

This is useful for data stores that have less structured data, like NoSQL databases.

You can define nested objects by using the `csValue` function to define a value in a nested object. The value naming convention of the `csValue` function is a dot-separated string of the nested object path, e.g. `profile.name` or `profile.address.street`.
Expand All @@ -105,15 +125,15 @@ export const protectedUsers = csTable("users", {
```

When working with nested objects:
- Searchable encryption is not supported on nested objects
- Searchable encryption is not supported on nested `csValue` objects (use `.searchableJson()` for searchable JSON)
- Each level can have its own encrypted fields
- The maximum nesting depth is 3 levels
- Null and undefined values are supported at any level
- Optional nested objects are supported

> [!WARNING]
> TODO: The schema builder does not validate the values you supply to the `csValue` or `csColumn` functions.
> These values are meant to be unique, and and cause unexpected behavior if they are not defined correctly.
> The schema builder does not currently validate the values you supply to the `csValue` or `csColumn` functions.
> These values must be unique within your schema - duplicate values may cause unexpected behavior.

## Available index options

Expand All @@ -124,8 +144,12 @@ The following index options are available for your schema:
| equality | Enables a exact index for equality queries. | `WHERE email = 'example@example.com'` |
| freeTextSearch | Enables a match index for free text queries. | `WHERE description LIKE '%example%'` |
| orderAndRange | Enables an sorting and range queries index. | `ORDER BY price ASC` |
| searchableJson | Enables searching inside JSON columns. | `WHERE data->'user'->>'email' = '...'` |

You can chain these methods to your column to configure them in any combination.
You can chain `equality()`, `freeTextSearch()`, and `orderAndRange()` methods in any combination.

> [!WARNING]
> `searchableJson()` is **mutually exclusive** with other index types. Do not combine `searchableJson()` with `equality()`, `freeTextSearch()`, or `orderAndRange()` on the same column.

## Initializing the Protect client

Expand Down
Loading