EQL supports encrypting, decrypting, and searching JSON and JSONB objects using structured encryption (ste_vec).
- Configuring the index
- Querying JSONB data with EQL
- EQL functions for JSONB and
ste_vec - How ste_vec indexing works
To enable searchable operations on encrypted JSONB data, configure an ste_vec index with the jsonb cast type.
SELECT eql_v2.add_search_config(
'users',
'encrypted_json',
'ste_vec',
'jsonb',
'{"prefix": "users/encrypted_json"}'
);The prefix option is required and should be unique per table/column combination (typically "table/column").
You can read more about the index configuration options here.
When inserting JSON data through CipherStash Proxy or Protect.js, wrap the data in the EQL payload format:
INSERT INTO users (encrypted_json) VALUES (
'{"v":2,"k":"pt","p":"{\"name\":\"John Doe\",\"metadata\":{\"age\":42}}","i":{"t":"users","c":"encrypted_json"}}'
);Data is stored in the database with encrypted ste_vec indexes:
{
"i": {
"c": "encrypted_json",
"t": "users"
},
"k": "sv",
"v": 2,
"sv": [["encrypted_term_1"], ["encrypted_term_2"], ...]
}When querying through CipherStash Proxy or Protect.js, the encrypted column is automatically decrypted:
SELECT encrypted_json FROM users;EQL provides specialized functions and operators to work with encrypted JSONB data.
Use PostgreSQL's containment operators directly on eql_v2_encrypted columns to check if one JSONB structure contains another.
Example: Check if column contains structure
Suppose we have encrypted JSONB data:
{
"top": {
"nested": ["a", "b", "c"]
}
}Query records that contain a specific structure:
SELECT * FROM examples
WHERE encrypted_json @> '{"v":2,"k":"pt","p":"{\"top\":{\"nested\":[\"a\"]}}","i":{"t":"examples","c":"encrypted_json"},"q":"ste_vec"}'::eql_v2_encrypted;Equivalent plaintext query:
SELECT * FROM examples
WHERE jsonb_column @> '{"top":{"nested":["a"]}}';Note: The @> operator checks if the left value contains the right value. The <@ operator checks the reverse (if left is contained in right).
For better performance on large tables, create a GIN index and use the jsonb_array() function:
-- Create GIN index
CREATE INDEX idx_encrypted_jsonb_gin
ON examples USING GIN (eql_v2.jsonb_array(encrypted_json));
ANALYZE examples;
-- Query using the GIN index
SELECT * FROM examples
WHERE eql_v2.jsonb_array(encrypted_json) @>
eql_v2.jsonb_array($1::eql_v2_encrypted);See GIN Indexes for JSONB Containment for complete setup instructions.
Extract fields from encrypted JSONB using selector hashes. Selectors are generated during encryption and identify specific JSON paths.
Function signature:
eql_v2.jsonb_path_query(val eql_v2_encrypted, selector text) RETURNS SETOF eql_v2_encryptedExample:
-- Extract all records where selector 'abc123...' exists
SELECT eql_v2.jsonb_path_query(encrypted_json, 'abc123def456...')
FROM examples;
-- Get first match only
SELECT eql_v2.jsonb_path_query_first(encrypted_json, 'abc123def456...')
FROM examples;
-- Check if selector exists
SELECT eql_v2.jsonb_path_exists(encrypted_json, 'abc123def456...')
FROM examples;Note: Selectors are hash-based identifiers for JSON paths, not the actual path strings like $.field. They are generated during encryption by CipherStash Proxy/Protect.js.
Use standard PostgreSQL JSON operators on encrypted columns:
-- Extract field by selector (returns eql_v2_encrypted)
SELECT encrypted_json->'selector_hash' FROM examples;
-- Extract field as text (returns encrypted value as text)
SELECT encrypted_json->>'selector_hash' FROM examples;
-- Extract array element by index (0-based, returns eql_v2_encrypted)
SELECT encrypted_array->0 FROM examples;Note: The -> operator supports integer array indexing (e.g., encrypted_array->0), but the ->> operator does not. Use -> to access array elements by index.
EQL supports array operations on encrypted JSONB arrays:
Get array length:
SELECT eql_v2.jsonb_array_length(encrypted_array_field)
FROM examples;Get array elements:
-- Returns SETOF eql_v2_encrypted
SELECT eql_v2.jsonb_array_elements(encrypted_array_field)
FROM examples;
-- Returns SETOF text (ciphertext)
SELECT eql_v2.jsonb_array_elements_text(encrypted_array_field)
FROM examples;Example with jsonb_path_query:
-- First query the array field, then get its elements
SELECT eql_v2.jsonb_array_elements(
eql_v2.jsonb_path_query(encrypted_json, 'array_selector_hash')
)
FROM examples;Use eql_v2.grouped_value() aggregate function to group encrypted JSONB results:
SELECT eql_v2.grouped_value(
eql_v2.jsonb_path_query_first(encrypted_json, 'color_selector')::jsonb
) AS color,
COUNT(*)
FROM examples
GROUP BY eql_v2.jsonb_path_query_first(encrypted_json, 'color_selector');Result:
| color | count |
|---|---|
| {"k":"pt","p":"blue",...} | 3 |
| {"k":"pt","p":"green",...} | 2 |
| {"k":"pt","p":"red",...} | 1 |
-
eql_v2.ste_vec(val jsonb) RETURNS eql_v2_encrypted[]- Extracts the ste_vec index array from a JSONB payload
-
eql_v2.ste_vec(val eql_v2_encrypted) RETURNS eql_v2_encrypted[]- Extracts the ste_vec index array from an encrypted value
-
eql_v2.ste_vec_contains(a eql_v2_encrypted, b eql_v2_encrypted) RETURNS boolean- Returns true if all ste_vec terms in b exist in a
- This is the function backing the
@>operator
-
eql_v2.jsonb_path_query(val eql_v2_encrypted, selector text) RETURNS SETOF eql_v2_encrypted- Returns all encrypted elements matching the selector
-
eql_v2.jsonb_path_query_first(val eql_v2_encrypted, selector text) RETURNS eql_v2_encrypted- Returns the first encrypted element matching the selector
-
eql_v2.jsonb_path_exists(val eql_v2_encrypted, selector text) RETURNS boolean- Returns true if any element matches the selector
-
eql_v2.jsonb_array_length(val eql_v2_encrypted) RETURNS integer- Returns the length of an encrypted array
-
eql_v2.jsonb_array_elements(val eql_v2_encrypted) RETURNS SETOF eql_v2_encrypted- Returns each array element as an encrypted value
-
eql_v2.jsonb_array_elements_text(val eql_v2_encrypted) RETURNS SETOF text- Returns each array element's ciphertext as text
-
eql_v2.is_ste_vec_array(val eql_v2_encrypted) RETURNS boolean- Returns true if the value represents an encrypted array
-
eql_v2.is_ste_vec_value(val eql_v2_encrypted) RETURNS boolean- Returns true if the value is a single ste_vec element
-
eql_v2.to_ste_vec_value(val eql_v2_encrypted) RETURNS eql_v2_encrypted- Converts a ste_vec array with a single element to a regular encrypted value
-
eql_v2.selector(val eql_v2_encrypted) RETURNS text- Extracts the selector hash from an encrypted value
These functions enable efficient GIN-indexed containment queries. See GIN Indexes for JSONB Containment for index setup.
-
eql_v2.jsonb_array(val eql_v2_encrypted) RETURNS jsonb[]- Extracts encrypted JSONB as native PostgreSQL jsonb array for GIN indexing
- Create GIN indexes on this function for indexed containment queries
-
eql_v2.jsonb_contains(a eql_v2_encrypted, b eql_v2_encrypted) RETURNS boolean- GIN-indexed containment check: returns true if a contains b
- Alternative to
jsonb_array(a) @> jsonb_array(b)
-
eql_v2.jsonb_contained_by(a eql_v2_encrypted, b eql_v2_encrypted) RETURNS boolean- GIN-indexed reverse containment: returns true if a is contained by b
- Alternative to
jsonb_array(a) <@ jsonb_array(b)
eql_v2.grouped_value(jsonb) RETURNS jsonb- Aggregate function for grouping encrypted values (returns first non-null value in group)
Structured Encryption (ste_vec) creates searchable indexes for JSONB by:
- Flattening the JSON structure - Each unique path to a leaf value gets a selector (hash)
- Creating encrypted terms - Each path prefix and value is encrypted separately
- Storing as array - All encrypted terms are stored in the
sv(ste_vec) array
Example document:
{
"account": {
"email": "alice@example.com",
"roles": ["admin", "owner"]
}
}Creates selectors for:
$(root object)$.account(account object)$.account.email(email field)$.account.emailwith value "alice@example.com"$.account.roles(roles array)$.account.roles[](each role value)
Querying:
Containment queries (@>) check if all required encrypted terms exist in the target's ste_vec array. This enables queries like:
-- Find records where account.email = "alice@example.com"
WHERE encrypted_data @> '<encrypted_query_payload>'::eql_v2_encrypted
-- Find records where account.roles contains "admin"
WHERE encrypted_data @> '<encrypted_query_payload>'::eql_v2_encryptedThe actual encryption and selector generation is handled by CipherStash Proxy or Protect.js, not by EQL directly.