Skip to content

docs: add CREATE INDEX guide for encrypted columns (self-hosted vs Supabase paths differ) #4

@coderdan

Description

@coderdan

Summary

The new docs site doesn't currently cover how users should create PostgreSQL indexes on encrypted columns. CREATE INDEX appears zero times across content/ — grep-confirmed. This is a regression vs. the previous docs and a real user-facing gap because the correct index-creation pattern differs between self-hosted PostgreSQL and Supabase, and the wrong path silently degrades to sequential scans even when EQL is correctly installed.

Context

EQL ships in two build variants:

  • Full (cipherstash-encrypt.sql) — installs custom btree and hash operator classes (encrypted_operator_class, encrypted_hash_operator_class) on the eql_v2_encrypted type. This lets users index encrypted columns directly: CREATE INDEX ... USING btree (encrypted_col).

  • Supabase (cipherstash-encrypt-supabase.sql) — omits all CREATE OPERATOR CLASS / CREATE OPERATOR FAMILY statements because they require superuser. On Supabase, the only working index path is functional indexes that wrap the column with an EQL extraction function:

    CREATE INDEX users_email_hmac_idx
    ON users USING hash (eql_v2.hmac_256(email));
    
    CREATE INDEX users_email_bloom_idx
    ON users USING gin (eql_v2.bloom_filter(email));

    Queries must then be written in the matching wrapped form:

    -- Hits the hash index above
    SELECT * FROM users
     WHERE eql_v2.hmac_256(email) = eql_v2.hmac_256('alice@example.com'::jsonb::eql_v2_encrypted);
    
    -- Bare equality on the column does NOT hit the functional index.
    -- On Supabase this falls back to a sequential scan.
    SELECT * FROM users WHERE email = 'alice@example.com'::jsonb::eql_v2_encrypted;

What's currently in the docs

Page What it says about indexes Gap
supabase.mdx Mentions CREATE OPERATOR FAMILY is omitted as a technical setup note. Doesn't connect this to "here's how to create indexes that actually work."
searchable-encryption.mdx:134 One Callout: "If your PostgreSQL database does not support EQL Operator families, use the eql_v2.ore_block_u64_8_256() function for ORDER BY." Single sentence, ORDER BY only — doesn't cover equality, pattern match, or the general principle.
eql-guide.mdx:112 "EQL leverages PostgreSQL's native indexing capabilities ... B-tree for exact/range, GIN for pattern matching." Abstract — never shows the actual CREATE INDEX statements.

There's nothing telling a user:

  • Which functional indexes to create for which query type
  • The query-form requirement (must use wrapped eql_v2.hmac_256(col) form on Supabase)
  • That self-hosted users have a simpler CREATE INDEX ... USING btree (col) option that doesn't exist on Supabase
  • The performance consequences of getting it wrong (silently slow queries)

Suggested page structure

A new page /stack/cipherstash/encryption/indexes (or similar), cross-linked from supabase.mdx and searchable-encryption.mdx. Core table:

Query type Self-hosted (full EQL) Supabase
Equality USING btree (col) (uses opclass) or USING hash (eql_v2.hmac_256(col)) USING hash (eql_v2.hmac_256(col)) only
Range / ORDER BY USING btree (col) None today — being addressed by upcoming OPE-index work
Pattern match USING gin (eql_v2.bloom_filter(col)) Same
JSONB containment USING gin (eql_v2.ste_vec(col)) Same

Plus a Supabase-specific Callout with the query-form requirement — bare WHERE col = … doesn't engage functional indexes. Queries must wrap the column with the same extraction function used in the index.

Why this matters now

  • A recent EQL change (perf: register cross-type btree/hash operators with eql_v2 opfamilies encrypt-query-language#186) makes bare-jsonb equality on encrypted columns work via direct btree indexes — but only on self-hosted, because the cross-type opfamily registration is in the same Supabase-excluded file. This widens the behavioural gap between the two deployments.
  • The OPE-index work in flight will introduce a second Supabase-specific guidance area (range/ORDER BY) that needs the same treatment.

Reference material to draw from

  • tests/sqlx/fixtures/bench_setup.sql in the EQL repo — currently the only canonical example of all five index types in one place.
  • tests/sqlx/fixtures/drop_operator_classes.sql — the fixture that simulates the Supabase environment (drops the opclasses to match what the supabase build doesn't install).

Out of scope

  • The OPE-index path for range queries on Supabase — separate work; once it lands, it'll need its own row in the table above.
  • Migration / upgrade docs from the previous index-creation pages — likely a clean rewrite is faster than porting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions