Schema definition
Define which columns to encrypt, what queries to support, and how to handle nested objects
Schema definition
Schemas tell the SDK which database columns to encrypt and what types of queries to support on the encrypted data.
Creating schema files
Declare your encryption schema in TypeScript — either in a single file or split across multiple files:
src/encryption/
└── schema.ts # single filesrc/encryption/schemas/
├── users.ts # per-table files
└── posts.tsDefining a schema
A schema maps your database tables and columns using encryptedTable and encryptedColumn:
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema"
// TypeScript name Database table name
// ↓ ↓
export const protectedUsers = encryptedTable("users", {
// TypeScript name Database column name
// ↓ ↓
email: encryptedColumn("email"),
})Index types
Full searchable encryption is only supported in Postgres. See Searchable encryption for details.
Index types determine what queries you can run on encrypted data. Methods are chainable — call as many as you need on a single column.
export const protectedUsers = encryptedTable("users", {
email: encryptedColumn("email")
.equality() // exact match queries
.freeTextSearch() // full-text search
.orderAndRange(), // sorting and range queries
})| Method | Purpose | SQL equivalent |
|---|---|---|
.equality() | Exact match lookups | WHERE email = '[email protected]' |
.freeTextSearch() | Full-text / fuzzy search | WHERE description LIKE '%example%' |
.orderAndRange() | Sorting, comparison, range queries | ORDER BY price ASC |
.searchableJson() | Encrypted JSONB path and containment queries | WHERE metadata @> '{"role":"admin"}' |
Only enable the indexes you need — each additional index type has a performance cost.
Equality token filters
The .equality() method accepts an optional array of token filters that are applied before indexing:
email: encryptedColumn("email").equality([{ kind: "downcase" }])| Filter | Description |
|---|---|
{ kind: 'downcase' } | Converts values to lowercase before comparison, enabling case-insensitive equality matching |
For columns storing JSON data, .searchableJson() is the recommended index. It automatically configures the column for encrypted JSONB path and containment queries. See Searchable encryption for details.
Data types
Use .dataType() to specify the plaintext type for a column:
encryptedColumn("age").dataType("number").orderAndRange()| Data type | Description |
|---|---|
'string' | Text values. This is the default. |
'text' | Long-form text values. |
'number' | Numeric values (integers and floats). |
'boolean' | Boolean true / false values. |
'date' | Date or timestamp values. |
'bigint' | Large integer values. |
'json' | JSON objects. Automatically set when using .searchableJson(). |
Free-text search options
Customize the tokenizer and filter settings for .freeTextSearch():
encryptedColumn("bio").freeTextSearch({
tokenizer: { kind: "ngram", token_length: 3 }, // or { kind: "standard" }
token_filters: [{ kind: "downcase" }],
k: 6,
m: 2048,
include_original: false,
})| Option | Type | Default | Description |
|---|---|---|---|
tokenizer | { kind: 'standard' } or { kind: 'ngram', token_length: number } | { kind: 'ngram', token_length: 3 } | Tokenization strategy |
token_filters | TokenFilter[] | [{ kind: 'downcase' }] | Filters applied to tokens before indexing |
k | number | 6 | Number of hash functions for the bloom filter |
m | number | 2048 | Size of the bloom filter in bits |
include_original | boolean | true | Whether to include the original value in the index |
Nested objects
CipherStash Encryption supports nested objects in your schema, allowing you to encrypt nested properties. You can define nested objects up to 3 levels deep using encryptedField.
Searchable encryption is not supported on nested objects. This is most useful for NoSQL databases or less structured data.
Using nested objects is not recommended for SQL databases. You should either use a JSON data type and encrypt the entire object, or use a separate column for each nested property.
import { encryptedTable, encryptedColumn, encryptedField } from "@cipherstash/stack/schema"
export const protectedUsers = encryptedTable("users", {
email: encryptedColumn("email").equality().freeTextSearch(),
profile: {
name: encryptedField("profile.name"),
address: {
street: encryptedField("profile.address.street"),
location: {
coordinates: encryptedField("profile.address.location.coordinates"),
},
},
},
})When working with nested objects:
- Each level can have its own encrypted fields
- The maximum nesting depth is 3 levels
- Null and undefined values are supported at any level
- Optional nested objects are supported
The schema builder does not validate the values you supply to the encryptedField or encryptedColumn functions. These values are meant to be unique, and may cause unexpected behavior if they are not defined correctly.
Encrypted JSONB
For columns that store JSON objects, use .searchableJson() to enable encrypted JSONB queries:
const documents = encryptedTable("documents", {
metadata: encryptedColumn("metadata").searchableJson(),
})This enables both JSONPath selector queries and containment queries on the encrypted data.
Multiple tables
Pass multiple schemas when initializing the client:
import { Encryption } from "@cipherstash/stack"
const client = await Encryption({ schemas: [protectedUsers, documents] })Type inference
Infer plaintext and encrypted types from your schema:
import type { InferPlaintext, InferEncrypted } from "@cipherstash/stack/schema"
type UserPlaintext = InferPlaintext<typeof protectedUsers>
// { email: string; ... }
type UserEncrypted = InferEncrypted<typeof protectedUsers>
// { email: Encrypted; ... }Client-safe exports
For client-side code where the native FFI module is not available, import schema builders from the @cipherstash/stack/client subpath:
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/client"This exports schema builders and types only — no native module dependency.