Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de69cf4
docs: 📝 add Drizzle ORM integration guide
calvinbrewer Oct 30, 2025
08933a1
test: ✅ add Drizzle ORM integration test and update Supabase tests
calvinbrewer Oct 30, 2025
55dd990
fix: 🚨 replace isNaN with Number.isNaN and fix test any type
calvinbrewer Oct 30, 2025
86e9fda
chore: 📦️ update package.json and lockfile
calvinbrewer Oct 30, 2025
701f716
docs: 📝 update Drizzle ORM integration guide with encrypted type and …
calvinbrewer Oct 30, 2025
9112af9
refactor: ♻️ extract Drizzle ORM integration into separate package
calvinbrewer Nov 4, 2025
3ae95b4
Apply suggestion from @coderdan
calvinbrewer Nov 4, 2025
21fd753
Apply suggestion from @coderdan
calvinbrewer Nov 4, 2025
86e3311
feat: ✨ add batched operator support via protectOps.and()
calvinbrewer Nov 4, 2025
6324cf3
Merge branch 'drizzle-orm' of https://github.com/cipherstash/protectj…
calvinbrewer Nov 4, 2025
a99517b
docs: 📝 add type safety documentation for encryptedType<T>
calvinbrewer Nov 4, 2025
8150599
fix: 🐛 enable LazyOperatorPromise to work standalone and in and()
calvinbrewer Nov 4, 2025
0092372
docs: 📝 document mixing regular Drizzle operators with Protect operat…
calvinbrewer Nov 4, 2025
76325e1
chore: refactor operators
calvinbrewer Nov 5, 2025
9b36560
ci: 👷 add CI database setup for drizzle package tests
calvinbrewer Nov 5, 2025
12c6821
test: ✅ skip order by tests for CI database compatibility
calvinbrewer Nov 5, 2025
60b64b9
docs: 📝 add comprehensive README for drizzle package integration
calvinbrewer Nov 5, 2025
a0e7c86
chore: ➖ remove drizzle-orm and postgres from protect package devDepe…
calvinbrewer Nov 5, 2025
d8ed4d4
chore: changeset
calvinbrewer Nov 5, 2025
33d5789
feat(examples): update drizzle example with fintech use case
calvinbrewer Nov 5, 2025
5ce557c
chore: pr feedback
calvinbrewer Nov 6, 2025
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
5 changes: 5 additions & 0 deletions .changeset/many-bikes-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/drizzle": minor
---

Released initial Drizzle ORM interface.
5 changes: 5 additions & 0 deletions .changeset/petite-places-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/schema": minor
---

Exported all types for packages looking for deeper integrations with Protect.js.
9 changes: 9 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ jobs:
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect-dynamodb/.env
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect-dynamodb/.env

- name: Create .env file in ./packages/drizzle/
run: |
touch ./packages/drizzle/.env
echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/drizzle/.env
echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/drizzle/.env
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/drizzle/.env
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/drizzle/.env
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> ./packages/drizzle/.env

# Run TurboRepo tests
- name: Run tests
run: pnpm run test
304 changes: 290 additions & 14 deletions examples/drizzle/README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,319 @@
# drizzle-eql
# Express REST API with Drizzle ORM and Protect.js

This is a example using the [drizzle-orm](https://drizzle-orm.com/).
This example demonstrates a FinTech REST API built with Express.js, Drizzle ORM, and Protect.js. It showcases how to encrypt sensitive financial data (account numbers, amounts, transaction descriptions) while maintaining the ability to search and query encrypted fields.

## Prerequisites

- PostgreSQL database
- CipherStash credentials and account
- **Node.js**: >= 22
- **PostgreSQL**: Database with EQL v2 functions installed
- **CipherStash account**: For encryption credentials

## Technologies

- [Express](https://expressjs.com/) - Web framework
- [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM
- [Protect.js](https://github.com/cipherstash/protectjs) - End-to-end encryption
- [PostgreSQL](https://www.postgresql.org/) - Database

## Setup

1. Create a PostgreSQL database and a user with read and write permissions.
2. Create a `.env` file in the root directory of the project with the following content:
### 1. Install Dependencies

```bash
pnpm install
```

### 2. Set Up PostgreSQL with EQL v2

Before running migrations, you need to install the EQL v2 types and functions in your PostgreSQL database:

```bash
# Download the EQL install script
curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql

# Install EQL types and functions
psql -d your_database -f cipherstash-encrypt.sql
```

This creates the `eql_v2_encrypted` composite type and search functions needed for searchable encryption.

### 3. Environment Variables

Create an account on [CipherStash](https://dashboard.cipherstash.com/sign-up), create a workspace, then generate your client credentials.

Then, create a `.env` file in the root directory:

```bash
# Database connection
DATABASE_URL="postgresql://[username]:[password]@[host]:5432/[database]"

# CipherStash credentials (generated in your CipherStash workspace)
CS_CLIENT_ID=[client-id]
CS_CLIENT_KEY=[client-key]
CS_WORKSPACE_ID=[workspace-id]
CS_WORKSPACE_CRN=[workspace-crn]
CS_CLIENT_ACCESS_KEY=[access-key]

# Optional: Server port (default: 3000)
PORT=3000
```

3. Run the following command to install the dependencies:
### 4. Run Database Migrations

```bash
npm install
pnpm db:migrate
```

4. Run the following command to insert a new user with an encrypted email:
This creates the `transactions` table with encrypted columns.

### 5. Start the Server

```bash
npx tsx src/insert.ts --email [email protected]
pnpm dev
```

5. Run the following command to select all the encrypted emails from the database:
The server will start on `http://localhost:3000` (or the port specified in `PORT`).

## API Endpoints

### Health Check

**GET** `/health`

Returns server status.

```bash
npx tsx src/select.ts
curl http://localhost:3000/health
```

Response:
```json
{
"status": "ok",
"message": "Server is running"
}
```

### List Transactions

**GET** `/transactions`

Retrieves all transactions with optional filters.

**Query Parameters:**
- `accountNumber` (string) - Search by account number (encrypted field, text search)
- `minAmount` (number) - Minimum transaction amount (encrypted field, range query)
- `maxAmount` (number) - Maximum transaction amount (encrypted field, range query)
- `status` (string) - Filter by status (non-encrypted field)

**Example:**
```bash
# Get all transactions
curl http://localhost:3000/transactions

# Filter by account number
curl "http://localhost:3000/transactions?accountNumber=1234"

# Filter by amount range
curl "http://localhost:3000/transactions?minAmount=100&maxAmount=1000"

# Filter by status
curl "http://localhost:3000/transactions?status=completed"
```
Comment on lines +110 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for demo but maybe add a comment that params should usually be done via POST if they are sensitive so that they don't appear in logs.


> [!IMPORTANT]
> For production use, you should not use GET requests to filter data.
> Instead, you should use POST requests to filter data so sensitive data is not exposed in the URL.

**Response:**
```json
{
"transactions": [
{
"id": 1,
"accountNumber": "1234567890",
"amount": 500.00,
"description": "Payment for services",
"transactionType": "payment",
"status": "completed",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
]
}
```

### Create Transaction

**POST** `/transactions`

Creates a new transaction with encrypted sensitive fields.

**Request Body:**
```json
{
"accountNumber": "1234567890",
"amount": 500.00,
"description": "Payment for services",
"transactionType": "payment",
"status": "pending"
}
```

**Example:**
```bash
curl -X POST http://localhost:3000/transactions \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "1234567890",
"amount": 500.00,
"description": "Payment for services",
"transactionType": "payment",
"status": "pending"
}'
```

**Response:**
```json
{
"transaction": {
"id": 1,
"accountNumber": "1234567890",
"amount": 500.00,
"description": "Payment for services",
"transactionType": "payment",
"status": "pending",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
```

### Get Transaction by ID

**GET** `/transactions/:id`

Retrieves a single transaction by ID.

**Example:**
```bash
curl http://localhost:3000/transactions/1
```

**Response:**
```json
{
"transaction": {
"id": 1,
"accountNumber": "1234567890",
"amount": 500.00,
"description": "Payment for services",
"transactionType": "payment",
"status": "completed",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
```

### Update Transaction

**PUT** `/transactions/:id`

Updates a transaction. All fields are optional.

**Request Body:**
```json
{
"accountNumber": "9876543210",
"amount": 750.00,
"description": "Updated description",
"transactionType": "refund",
"status": "completed"
}
```

**Example:**
```bash
curl -X PUT http://localhost:3000/transactions/1 \
-H "Content-Type: application/json" \
-d '{
"status": "completed",
"amount": 750.00
}'
```

**Response:**
```json
{
"transaction": {
"id": 1,
"accountNumber": "1234567890",
"amount": 750.00,
"description": "Payment for services",
"transactionType": "payment",
"status": "completed",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T11:00:00Z"
}
}
```

### Delete Transaction

**DELETE** `/transactions/:id`

Deletes a transaction.

**Example:**
```bash
curl -X DELETE http://localhost:3000/transactions/1
```

**Response:** 204 No Content

## Database Schema

The `transactions` table has the following structure:

- **Encrypted fields** (using `eql_v2_encrypted` type):
- `account_number` - Account number with equality and text search
- `amount` - Transaction amount with equality, range queries, and sorting
- `description` - Transaction description with text search

- **Non-encrypted fields**:
- `id` - Primary key (serial)
- `transaction_type` - Type of transaction (varchar)
- `status` - Transaction status (varchar, default: 'pending')
- `created_at` - Timestamp
- `updated_at` - Timestamp

## How It Works

### Encryption

- Sensitive fields (`accountNumber`, `amount`, `description`) are encrypted using Protect.js before being stored in the database
- The `@cipherstash/drizzle` package provides `encryptedType` helper to define encrypted columns in Drizzle schemas
- Data is automatically encrypted when inserting/updating and decrypted when reading

### Searchable Encryption

- The API demonstrates searchable encryption capabilities:
- **Text search** on `accountNumber` and `description` using `ilike` operator
- **Range queries** on `amount` using `gte` and `lte` operators
- **Equality queries** on `accountNumber` and `amount`
- All encrypted field queries use Protect.js operators that automatically handle encryption

### Type Safety

- TypeScript types are preserved throughout the encryption/decryption process
- The `encryptedType<T>` helper ensures decrypted values maintain their correct types

## Notes

- **Native Module**: Protect.js uses `@cipherstash/protect-ffi`, a native Node-API module. Express doesn't bundle code, so no special configuration is needed. If deploying to serverless platforms, ensure the native module is properly externalized.
- **Error Handling**: All Protect.js operations return a Result type (`{ data }` or `{ failure }`). The API properly handles these results and returns appropriate HTTP status codes.
- **Bulk Operations**: The API uses `bulkEncryptModels` and `bulkDecryptModels` for efficient batch operations when querying multiple transactions.

## License

This project is licensed under the MIT License.
This project is licensed under the MIT License.
15 changes: 15 additions & 0 deletions examples/drizzle/drizzle/0001_transactions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Drop old users table if it exists
DROP TABLE IF EXISTS "users";

-- Create transactions table
CREATE TABLE IF NOT EXISTS "transactions" (
"id" serial PRIMARY KEY NOT NULL,
"account_number" eql_v2_encrypted,
"amount" eql_v2_encrypted,
"description" eql_v2_encrypted,
"transaction_type" varchar(50) NOT NULL,
"status" varchar(20) NOT NULL DEFAULT 'pending',
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);

7 changes: 7 additions & 0 deletions examples/drizzle/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"when": 1734712905691,
"tag": "0000_goofy_cannonball",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1734712905700,
"tag": "0001_transactions",
"breakpoints": true
}
]
}
Loading