Summary
Adopt EQL V3's self-configuring domain types in the @cipherstash/stack schema builder, consuming the new @cipherstash/eql package (published from the EQL repo — see that repo's companion issue).
EQL V3 replaces the per-column index-configuration model with self-configuring Postgres domain types (int4_eq, text_match, text_search, …). The type name encodes the searchable-encryption metadata, so:
- the types are Postgres Domains, which ORMs can read as
jsonb; and
- the column is inherently self-configured — the searchable-encryption metadata no longer has to be configured client-side, because the type name specifies it (e.g.
int4_eq always carries an HMAC equality term).
Current state
The going-forward builder is @cipherstash/stack/schema (packages/stack/src/schema/index.ts). It builds an EncryptConfig {v:1} with per-column cast_as + indexes: {ore, unique, match, ste_vec}, validates it, and passes it to protect-ffi at newClient(...) time (packages/stack/src/encryption/index.ts). It already has the seam for this migration: an eqlCastAsEnum (text/int/small_int/big_int/real/double/boolean/date/jsonb) and a toEqlCastAs() SDK→EQL mapper.
There is no EQL V3 footprint in the repo today — everything is EQL v2 (eql_v2 schema, eql_v2_encrypted composite type, eql_v2_configuration state machine, vendored eql-2.3.1 bundle).
Scope note. This issue targets the primary @cipherstash/stack/schema builder. The legacy standalone @cipherstash/schema (packages/schema) — used by @cipherstash/protect, now in maintenance mode — is intentionally out of scope here; migrate it separately if desired.
Goal
Migrate @cipherstash/stack/schema to depend on @cipherstash/eql and represent/emit V3 domain types instead of the index-config model, so that choosing a column's capabilities selects a self-configuring domain type rather than hand-assembling an indexes object.
Scope (primary)
- Add
@cipherstash/eql as a dependency and type column payloads with its generated types.
- Map the builder API → V3 domain types, e.g.:
number + equality + range → int8_ord_ore (or width-appropriate int4_*)
string + match → text_match
string + equality + match + range → text_search
- storage-only → the bare domain (
int4, text, bool, …)
- Decide whether the public API exposes the domain types directly or keeps the capability-builder (
.equality() / .orderAndRange() / .freeTextSearch()) and infers the domain type from the chosen capabilities (eqlCastAsEnum / toEqlCastAs is the existing seam).
- Replace the
indexes object / EncryptConfig {v:1} emission accordingly.
Knock-on: docs / README (explicitly in scope)
README.md (top-level quick-start + "Searchable encryption"), packages/stack/README.md + CHANGELOG.md.
AGENTS.md, .cursorrules, docs/plans/encryption-migrations.md.
- Agent skills:
skills/stash-encryption, stash-drizzle, stash-dynamodb, stash-supabase, stash-cli (SKILL.md).
- Examples:
examples/basic, examples/prisma, examples/supabase-worker.
- Add a changeset (repo uses Changesets,
access: restricted).
Downstream impact (flagged — likely separate follow-up issues)
The schema builder is the entry point, but switching to self-configuring types ripples through the stack. Capturing these so they aren't lost:
- protect-ffi owns
cast_as-driven encoding and must accept/encode V3 domain types — coordinate a lockstep version bump (note the 7-day minimumReleaseAge cooldown excludes @cipherstash/protect-ffi*).
- FFI init —
encryptConfigSchema hard-locks v: z.literal(1); packages/stack/src/wasm-inline.ts normalizeCastAs() rewrites cast_as via toEqlCastAs before the WASM newClient.
- Proxy / DB config state machine —
packages/cli db/push.ts (toEqlConfig → public.eql_v2_configuration, pending/activate dance) and @cipherstash/migrate src/eql.ts (eql_v2.* wrappers). Self-configuring types may remove or reshape this.
- Drizzle —
packages/drizzle/src/pg/* hardcodes the eql_v2_encrypted type name and eql_v2.* operators (eq/gt/order_by/jsonb_path_query_first/…) and the composite-literal codec.
- prisma-next — vendored
eql-2.3.1 bundle (src/migration/eql-install.generated.ts), cipherstash/<type>@1 codec ids, and EQL_V2_* constants.
- DynamoDB —
protect-dynamodb / stack/src/dynamodb branch on cast_as === 'json' + indexes.ste_vec.
Acceptance criteria
@cipherstash/stack/schema depends on @cipherstash/eql and represents columns as V3 domain types (self-configuring) rather than the indexes config object.
- Docs, README, examples, and skills are updated to the V3 type model.
- A changeset is added; downstream-impact items above are filed as follow-up issues.
Dependency
Blocked on @cipherstash/eql being published from the EQL repo: cipherstash/encrypt-query-language#331
Summary
Adopt EQL V3's self-configuring domain types in the
@cipherstash/stackschema builder, consuming the new@cipherstash/eqlpackage (published from the EQL repo — see that repo's companion issue).EQL V3 replaces the per-column index-configuration model with self-configuring Postgres domain types (
int4_eq,text_match,text_search, …). The type name encodes the searchable-encryption metadata, so:jsonb; andint4_eqalways carries an HMAC equality term).Current state
The going-forward builder is
@cipherstash/stack/schema(packages/stack/src/schema/index.ts). It builds anEncryptConfig {v:1}with per-columncast_as+indexes: {ore, unique, match, ste_vec}, validates it, and passes it to protect-ffi atnewClient(...)time (packages/stack/src/encryption/index.ts). It already has the seam for this migration: aneqlCastAsEnum(text/int/small_int/big_int/real/double/boolean/date/jsonb) and atoEqlCastAs()SDK→EQL mapper.There is no EQL V3 footprint in the repo today — everything is EQL v2 (
eql_v2schema,eql_v2_encryptedcomposite type,eql_v2_configurationstate machine, vendoredeql-2.3.1bundle).Goal
Migrate
@cipherstash/stack/schemato depend on@cipherstash/eqland represent/emit V3 domain types instead of the index-config model, so that choosing a column's capabilities selects a self-configuring domain type rather than hand-assembling anindexesobject.Scope (primary)
@cipherstash/eqlas a dependency and type column payloads with its generated types.number+ equality + range →int8_ord_ore(or width-appropriateint4_*)string+ match →text_matchstring+ equality + match + range →text_searchint4,text,bool, …).equality()/.orderAndRange()/.freeTextSearch()) and infers the domain type from the chosen capabilities (eqlCastAsEnum/toEqlCastAsis the existing seam).indexesobject /EncryptConfig {v:1}emission accordingly.Knock-on: docs / README (explicitly in scope)
README.md(top-level quick-start + "Searchable encryption"),packages/stack/README.md+CHANGELOG.md.AGENTS.md,.cursorrules,docs/plans/encryption-migrations.md.skills/stash-encryption,stash-drizzle,stash-dynamodb,stash-supabase,stash-cli(SKILL.md).examples/basic,examples/prisma,examples/supabase-worker.access: restricted).Downstream impact (flagged — likely separate follow-up issues)
The schema builder is the entry point, but switching to self-configuring types ripples through the stack. Capturing these so they aren't lost:
cast_as-driven encoding and must accept/encode V3 domain types — coordinate a lockstep version bump (note the 7-dayminimumReleaseAgecooldown excludes@cipherstash/protect-ffi*).encryptConfigSchemahard-locksv: z.literal(1);packages/stack/src/wasm-inline.tsnormalizeCastAs()rewritescast_asviatoEqlCastAsbefore the WASMnewClient.packages/clidb/push.ts(toEqlConfig→public.eql_v2_configuration, pending/activate dance) and@cipherstash/migratesrc/eql.ts(eql_v2.*wrappers). Self-configuring types may remove or reshape this.packages/drizzle/src/pg/*hardcodes theeql_v2_encryptedtype name andeql_v2.*operators (eq/gt/order_by/jsonb_path_query_first/…) and the composite-literal codec.eql-2.3.1bundle (src/migration/eql-install.generated.ts),cipherstash/<type>@1codec ids, andEQL_V2_*constants.protect-dynamodb/stack/src/dynamodbbranch oncast_as === 'json'+indexes.ste_vec.Acceptance criteria
@cipherstash/stack/schemadepends on@cipherstash/eqland represents columns as V3 domain types (self-configuring) rather than theindexesconfig object.Dependency
Blocked on
@cipherstash/eqlbeing published from the EQL repo: cipherstash/encrypt-query-language#331