Skip to content
Merged
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
4 changes: 3 additions & 1 deletion content/docs/reference/eql/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"filtering",
"sorting",
"grouping-and-aggregates",
"joins"
"joins",
"---Previous versions---",
"v2"
]
}
122 changes: 122 additions & 0 deletions content/docs/reference/eql/v2/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: EQL v2
description: PostgreSQL types, operators, and functions for querying encrypted data with EQL v2 (v2.2) — the eql_v2_encrypted type and searchable index types.
type: reference
components: [eql]
verifiedAgainst:
eql: "2.2"
---

<Callout type="info" title="EQL v2 reference">
This section documents **EQL v2** (v2.2), the release existing CipherStash deployments run. Starting a new project? Use **EQL v3** — see the [EQL reference](/reference/eql).
</Callout>

**Encrypt Query Language (EQL)** is a set of PostgreSQL types, operators, and functions that enable queries on encrypted data without decryption. EQL works seamlessly with the [CipherCell](/reference/eql/v2/payload) format to provide searchable encryption capabilities directly in PostgreSQL.

## What is EQL?

EQL provides the database-side components needed to query encrypted data. Unlike traditional PostgreSQL extensions, EQL is implemented as a collection of types, operators, and functions, making it compatible with managed database providers like AWS RDS that restrict extension installation.

When combined with the [Encryption SDK](/stack/cipherstash/encryption) or [CipherStash Proxy](/stack/cipherstash/proxy), EQL enables:

- **Exact match queries** using encrypted equality operators
- **Range queries** with order-preserving encryption
- **Pattern matching** using encrypted Bloom filters
- **Unique constraints** on encrypted columns
- **JSON/JSONB operations** on encrypted structured data

## Core components

### The `eql_v2_encrypted` type

The foundation of EQL is the `eql_v2_encrypted` data type, which stores [CipherCells](/reference/eql/v2/payload) containing encrypted data and searchable encrypted metadata.

```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email eql_v2_encrypted,
name eql_v2_encrypted
);
```

<Callout type="warn">
The `eql_v2_encrypted` type is required for searchable encryption in PostgreSQL. Regular `JSON` or `JSONB` types can store CipherCells but do not support encrypted queries.
</Callout>

### Operators

EQL provides PostgreSQL operators that work directly with encrypted data:

```sql
-- Exact match
SELECT * FROM users WHERE email = 'encrypted_search_value'::eql_v2_encrypted;

-- Range queries
SELECT * FROM products WHERE price > 'encrypted_value'::eql_v2_encrypted;

-- Pattern matching
SELECT * FROM documents WHERE content LIKE '%encrypted_pattern%';
```

### Functions

EQL ships in the `eql_v2` schema, with functions in three groups:

- **Configuration** — register tables, columns, and searchable indexes in the EQL configuration.
- **Index-term extraction** — `eql_v2.hmac_256()` (exact match), `eql_v2.bloom_filter()` (pattern matching), and `eql_v2.ore_block_u64_8_256()` (range) extract a searchable term from an encrypted value. These back the functional indexes — see [Setting up indexes](/reference/eql/v2/indexes).
- **Comparison** — operators (`=`, `<`, `LIKE`, `@>`, …) are the query surface over encrypted columns.

For the complete, per-version function reference — every signature, parameter, and return type — see the [EQL API reference](/stack/reference/eql/), generated from each EQL release.

## Index types

EQL supports multiple searchable encryption index types. Each index type enables different query patterns:

### `unique`: Exact match

Enables exact equality queries and unique constraints using HMAC-SHA256.

[Learn more about exact indexes](/stack/cipherstash/encryption/searchable-encryption#exact-match)

### `ore`: Range queries

Enables range comparisons (`<`, `>`, `BETWEEN`) and ordering (`ORDER BY`) using Order Revealing Encryption.

[Learn more about range indexes](/stack/cipherstash/encryption/searchable-encryption#range--order)

### `match`: Pattern matching

Enables substring and full-text search (`LIKE`, `ILIKE`) using encrypted Bloom filters with trigrams.

[Learn more about match indexes](/stack/cipherstash/encryption/searchable-encryption#match-pattern)

### `ste_vec`: Structured data

Enables containment queries and JSON-style operations on encrypted arrays and JSONB data.

## How it works

EQL leverages PostgreSQL's native indexing capabilities to enable efficient queries on encrypted data. The searchable encrypted metadata within [CipherCells](/reference/eql/v2/payload) is indexed using standard PostgreSQL index types (B-tree for exact/range, GIN for pattern matching).

When a query is executed:

1. **Client-side**: The application encrypts the search value using the same encryption scheme, producing a CipherCell with the appropriate searchable encrypted metadata
2. **Database-side**: EQL operators extract and compare the searchable encrypted metadata from both the stored CipherCells and the search CipherCell
3. **Result**: Matching rows are returned without ever decrypting the data in the database

## Compatibility

EQL is designed to work with:

- **PostgreSQL 14+**: Full support for all EQL features
- **Managed databases**: Works with AWS RDS, Azure Database, Google Cloud SQL, and other managed PostgreSQL providers
- **CipherStash SDKs**: Integrates with all CipherStash SDKs as well as CipherStash Proxy

<Callout type="info">
Unlike PostgreSQL extensions that require `CREATE EXTENSION`, EQL types and functions are installed directly into your database schema, making it compatible with managed database environments that restrict extension installation.
</Callout>

## Related documentation

- [CipherCell format](/reference/eql/v2/payload): The data structure used by EQL
- [Supported queries](/stack/cipherstash/encryption/searchable-encryption): Available searchable encryption schemes
154 changes: 154 additions & 0 deletions content/docs/reference/eql/v2/indexes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
title: Setting up indexes
description: Create PostgreSQL indexes for encrypted columns. Index syntax differs between self-hosted PostgreSQL and managed databases like Supabase.
type: reference
components: [eql]
verifiedAgainst:
eql: "2.2"
---

<Callout type="info">
EQL v2 reference, for existing v2.2 deployments. New projects use [EQL v3](/reference/eql).
</Callout>

Encrypted columns need PostgreSQL indexes for fast queries. Without an index, the database performs a sequential scan: correct but slow at scale.

Index syntax differs between deployment types. Self-hosted PostgreSQL with full EQL installed supports custom operator classes and can use B-tree indexes directly on `eql_v2_encrypted` columns. Managed databases like Supabase cannot install operator families (they require superuser), so indexes must use extraction functions instead.

## Deployment matrix

| Query type | Self-hosted (full EQL) | Supabase |
|---|---|---|
| Equality | `USING btree (col)` with opclass, or `USING hash (eql_v2.hmac_256(col))` | `USING hash (eql_v2.hmac_256(col))` only |
| Range / ORDER BY | `USING btree (col)` with opclass | None (OPE-index work in progress) |
| Pattern match | `USING gin (eql_v2.bloom_filter(col))` | Same |
| JSONB containment | `USING gin (eql_v2.ste_vec(col))` | Same |

<Callout type="info">
Range filters (`>`, `>=`, `<`, `<=`) work on Supabase without a range index (they use a sequential scan). `ORDER BY` on encrypted columns is not supported on Supabase at all. Sort application-side after decrypting results. Operator family support for Supabase is in development.
</Callout>

---

## Equality

Equality indexes speed up `WHERE col = $1` queries and `IN` lists.

**Self-hosted (B-tree with operator class):**

```sql
CREATE INDEX ON users USING btree (email);
```

This works because the full EQL install registers a B-tree operator class for `eql_v2_encrypted` that compares HMAC terms.

**Self-hosted or Supabase (hash on extraction function):**

```sql
CREATE INDEX ON users USING hash (eql_v2.hmac_256(email));
```

This form works on both deployment types. Use it when you want one index that works everywhere, or when you are on Supabase.

See queries: [Equality queries](/reference/eql/v2/queries#equality)

---

## Match

Match indexes speed up `WHERE col LIKE $1` and `ILIKE` queries. They use a GIN index on the Bloom filter extracted from each encrypted value.

```sql
CREATE INDEX ON users USING gin (eql_v2.bloom_filter(name));
```

This form is identical for self-hosted and Supabase.

See queries: [Match queries](/reference/eql/v2/queries#match-free-text)

---

## Range and order

Range indexes support `>`, `>=`, `<`, `<=`, `BETWEEN`, and `ORDER BY` on encrypted columns.

**Self-hosted (B-tree with operator class):**

```sql
CREATE INDEX ON users USING btree (age);
```

Requires the EQL operator family (`CREATE OPERATOR FAMILY`) to be installed. The full EQL install includes this. The `--exclude-operator-family` install flag omits it.

**Supabase:**

Functional range indexes for Supabase are not yet available. Range _filters_ work without an index (sequential scan). `ORDER BY` on encrypted columns is not supported on Supabase.

See queries: [Range queries](/reference/eql/v2/queries#range-and-ordering)

---

## JSONB

JSONB indexes support path existence and containment queries on encrypted JSON columns.

```sql
CREATE INDEX ON documents USING gin (eql_v2.ste_vec(metadata));
```

This form is identical for self-hosted and Supabase.

See queries: [JSONB queries](/reference/eql/v2/queries#jsonb-queries)

---

## Supabase query forms

This is the most common source of silent performance problems with encrypted columns on Supabase.

A functional index on `eql_v2.hmac_256(email)` is only engaged when the query uses the same extraction function. A bare `WHERE email = $1` query does not use the index, even if the index exists. The database falls back to a sequential scan: your query returns correct results, but it scans every row.

**Wrong (does not use functional index):**

```sql
SELECT * FROM users WHERE email = $1::eql_v2_encrypted;
```

**Right (engages the functional index):**

```sql
SELECT * FROM users WHERE eql_v2.hmac_256(email) = eql_v2.hmac_256($1::eql_v2_encrypted);
```

<Callout type="warn">
SDK wrappers (Drizzle adapter, Supabase wrapper) generate the correct query form automatically. This only matters when you write raw SQL queries against Supabase encrypted columns. If you are using the Drizzle adapter or Supabase wrapper, no action is needed.
</Callout>

The same principle applies to `eql_v2.bloom_filter` and `eql_v2.ste_vec` indexes: the extraction function must appear in both the index definition and the query predicate.

---

## Complete example

```sql filename="migrations/add_encrypted_indexes.sql"
-- Equality index (Supabase-compatible form)
CREATE INDEX users_email_eq_idx ON users USING hash (eql_v2.hmac_256(email));

-- Match index
CREATE INDEX users_name_match_idx ON users USING gin (eql_v2.bloom_filter(name));

-- JSONB index
CREATE INDEX documents_metadata_ste_idx ON documents USING gin (eql_v2.ste_vec(metadata));

-- Range index (self-hosted only — requires operator family)
CREATE INDEX users_age_range_idx ON users USING btree (age);
```

---

## Related

- [Searchable encryption queries](/reference/eql/v2/queries): Query patterns for each index type
- [Searchable encryption overview](/stack/cipherstash/encryption/searchable-encryption): How searchable indexes work
- [Supabase integration](/stack/cipherstash/supabase): Supabase-specific setup and limitations
- [EQL guide](/reference/eql/v2): Full reference for EQL types and functions
4 changes: 4 additions & 0 deletions content/docs/reference/eql/v2/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "EQL v2",
"pages": ["indexes", "queries", "payload"]
}
Loading