stash-forge
CipherStash Stack - Stash Forge
Configure and use @cipherstash/stack-forge for EQL database setup, encryption schema management, and Supabase integration.
Trigger
Use this skill when:
- The user asks about setting up CipherStash EQL in a database
- Code imports
@cipherstash/stack-forgeor referencesstash-forge - A
stash.config.tsfile exists or needs to be created - The user wants to install, configure, or manage the EQL extension in PostgreSQL
- The user mentions "stack-forge", "stash-forge", "EQL install", or "encryption schema"
Do NOT trigger when:
- The user is working with
@cipherstash/stack(the runtime SDK) without needing database setup - General PostgreSQL questions unrelated to CipherStash
What is @cipherstash/stack-forge?
@cipherstash/stack-forge is a dev-time CLI and TypeScript library for managing CipherStash EQL (Encrypted Query Language) in PostgreSQL databases. It is a companion to the @cipherstash/stack runtime SDK — it handles database setup during development while @cipherstash/stack handles runtime encryption/decryption operations.
Think of it like Prisma Migrate or Drizzle Kit: a dev-time tool that manages your database schema.
Configuration
1. Create stash.config.ts in the project root
import { defineConfig } from '@cipherstash/stack-forge'
export default defineConfig({
databaseUrl: process.env.DATABASE_URL!,
})
Config options
type StashConfig = {
databaseUrl: string // Required: PostgreSQL connection string
client?: string // Optional: path to encryption client (default: './src/encryption/index.ts')
}
defineConfig()provides TypeScript type-checking for the config file.clientpoints to the encryption client file used bystash-forge pushandstash-forge validateto load the encryption schema.- Config is loaded automatically from
stash.config.tsby walking up fromprocess.cwd()(liketsconfig.jsonresolution). .envfiles are loaded automatically viadotenvbefore config evaluation.
CLI Usage
The primary interface is the stash-forge CLI, run via npx:
npx stash-forge <command> [options]
init — Initialize CipherStash Forge in your project
Interactive wizard that scaffolds your project for CipherStash encryption.
npx stash-forge init
The wizard will:
- Check if
@cipherstash/stackis installed and prompt to install it (auto-detects npm/pnpm/yarn/bun) - Ask for your database URL (pre-fills from
DATABASE_URLenv var if set) - Ask which integration you're using (Drizzle ORM, Supabase, or plain PostgreSQL)
- Ask where to create the encryption client file (default:
./src/encryption/index.ts) - If the client file already exists, ask whether to keep it or overwrite it
- Let you choose between:
- Build a schema now — interactive wizard: table name, column names, data types (string/number/boolean/date/json), and search operations (exact match, order and range, free-text search) for each column
- Use a placeholder schema — generates an example
userstable withemailandnamecolumns
- Generate
stash.config.tsand the encryption client file - Print next steps with a link to the CipherStash dashboard for credentials
The generated client file uses the correct imports for the chosen integration:
- Drizzle:
encryptedType,extractEncryptionSchemafrom@cipherstash/stack/drizzle - Supabase/PostgreSQL:
encryptedTable,encryptedColumnfrom@cipherstash/stack/schema
install — Install EQL extension to the database
Uses bundled SQL by default for offline, deterministic installs. Three SQL variants are bundled:
cipherstash-encrypt.sql— standard install (default)cipherstash-encrypt-supabase.sql— Supabase-specific variantcipherstash-encrypt-no-operator-family.sql— no operator family variant
# Standard install
npx stash-forge install
# Reinstall even if already installed
npx stash-forge install --force
# Preview SQL without applying
npx stash-forge install --dry-run
# Supabase-compatible install (grants anon, authenticated, service_role)
npx stash-forge install --supabase
# Skip operator family (for non-superuser database roles)
npx stash-forge install --exclude-operator-family
# Fetch latest from GitHub instead of using bundled SQL
npx stash-forge install --latest
# Generate a Drizzle migration instead of direct install
npx stash-forge install --drizzle
# Drizzle migration with custom name and output directory
npx stash-forge install --drizzle --name setup-eql --out ./migrations
# Combine flags
npx stash-forge install --dry-run --supabase
Flags:
| Flag | Description |
|---|---|
--force |
Reinstall even if EQL is already installed |
--dry-run |
Print the SQL that would be executed without applying it |
--supabase |
Use Supabase-compatible install (no operator family + grants to Supabase roles) |
--exclude-operator-family |
Skip operator family creation (useful for non-superuser roles) |
--latest |
Fetch latest EQL from GitHub instead of using the bundled version |
--drizzle |
Generate a Drizzle migration instead of direct install |
--name <value> |
Migration name when using --drizzle (default: install-eql) |
--out <value> |
Drizzle output directory when using --drizzle (default: drizzle) |
install --drizzle
When --drizzle is passed, instead of connecting to the database directly, stash-forge:
- Runs
drizzle-kit generate --custom --name=<name>to scaffold an empty migration - Loads the bundled EQL install SQL (or downloads from GitHub with
--latest) - Writes the SQL into the generated migration file
You then run npx drizzle-kit migrate to apply it. Requires drizzle-kit as a dev dependency.
upgrade — Upgrade EQL extensions
Upgrade an existing EQL installation to the version bundled with the package (or latest from GitHub).
# Upgrade using bundled SQL
npx stash-forge upgrade
# Preview what would happen
npx stash-forge upgrade --dry-run
# Upgrade with Supabase-compatible SQL
npx stash-forge upgrade --supabase
# Fetch latest from GitHub
npx stash-forge upgrade --latest
Flags:
| Flag | Description |
|---|---|
--dry-run |
Show what would happen without making changes |
--supabase |
Use Supabase-compatible upgrade |
--exclude-operator-family |
Skip operator family creation |
--latest |
Fetch latest EQL from GitHub instead of bundled |
The EQL install SQL is idempotent and safe to re-run. The command checks the current version, re-runs the install SQL, then reports the new version. If EQL is not installed, it suggests running stash-forge install instead.
validate — Validate encryption schema
Validate your encryption schema for common misconfigurations.
# Basic validation
npx stash-forge validate
# Validate with Supabase context
npx stash-forge validate --supabase
# Validate with operator family exclusion context
npx stash-forge validate --exclude-operator-family
Flags:
| Flag | Description |
|---|---|
--supabase |
Check for Supabase-specific issues |
--exclude-operator-family |
Check for issues when operator families are excluded |
Validation rules:
| Rule | Severity | Description |
|---|---|---|
freeTextSearch on non-string column |
Warning | Free-text search only works with string data |
orderAndRange without operator families |
Warning | ORDER BY won't work without operator families |
| No indexes on encrypted column | Info | Column is encrypted but not searchable |
searchableJson without json data type |
Error | searchableJson requires dataType("json") |
Validation is also automatically run before push — issues are logged as warnings but don't block the push.
The validateEncryptConfig function and reportIssues helper are exported for programmatic use:
import { validateEncryptConfig, reportIssues } from '@cipherstash/stack-forge'
push — Push encryption schema to database (CipherStash Proxy only)
This command is only required when using CipherStash Proxy. If you're using the SDK directly (Drizzle, Supabase, or plain PostgreSQL), this step is not needed — the schema lives in your application code as the source of truth.
# Push schema to the database
npx stash-forge push
# Preview the schema as JSON without writing to the database
npx stash-forge push --dry-run
Flags:
| Flag | Description |
|---|---|
--dry-run |
Load and validate the schema, then print it as JSON. No database changes. |
When pushing, stash-forge:
- Loads the encryption client from the path in
stash.config.ts - Runs schema validation (warns but doesn't block)
- Transforms SDK data types to EQL-compatible
cast_asvalues (see table below) - Connects to Postgres and marks existing
eql_v2_configurationrows asinactive - Inserts the new config as an
activerow
SDK to EQL type mapping:
The SDK uses developer-friendly type names (e.g. 'string', 'number'), but EQL expects PostgreSQL-aligned types. The push command automatically maps these before writing to the database:
SDK type (dataType()) |
EQL cast_as |
|---|---|
string |
text |
text |
text |
number |
double |
bigint |
big_int |
boolean |
boolean |
date |
date |
json |
jsonb |
status — Show EQL installation status
npx stash-forge status
Reports:
- Whether EQL is installed and which version
- Database permission status
- Whether an active encrypt config exists in
eql_v2_configuration(only relevant for CipherStash Proxy)
test-connection — Test database connectivity
npx stash-forge test-connection
Verifies the database URL in your config is valid and the database is reachable. Reports:
- Database name
- Connected user/role
- PostgreSQL server version
Useful for debugging connection issues before running install or other commands.
Other commands (planned)
migrate— Run pending encrypt config migrations
Programmatic API
defineConfig(config: StashConfig): StashConfig
Identity function that provides type-safe configuration for stash.config.ts.
loadStashConfig(): Promise<ResolvedStashConfig>
Finds and loads stash.config.ts from the current directory or any parent. Validates with Zod. Applies defaults (e.g. client defaults to './src/encryption/index.ts'). Exits with code 1 if config is missing or invalid.
loadEncryptConfig(clientPath: string): Promise<EncryptConfig | undefined>
Loads the encryption client file, extracts the encrypt config, and returns it. Used by push and validate to build the schema JSON.
loadBundledEqlSql(options?): string
Load the bundled EQL install SQL as a string:
import { loadBundledEqlSql } from '@cipherstash/stack-forge'
const sql = loadBundledEqlSql() // standard
const sql = loadBundledEqlSql({ supabase: true }) // supabase variant
const sql = loadBundledEqlSql({ excludeOperatorFamily: true }) // no operator family
downloadEqlSql(excludeOperatorFamily?): Promise<string>
Download the latest EQL install SQL from GitHub releases.
EQLInstaller
import { EQLInstaller } from '@cipherstash/stack-forge'
const installer = new EQLInstaller({ databaseUrl: 'postgresql://...' })
installer.checkPermissions(): Promise<PermissionCheckResult>
Checks that the database role has the required permissions to install EQL.
type PermissionCheckResult = {
ok: boolean // true if all permissions are present
missing: string[] // list of missing permissions (empty if ok)
}
Required permissions (one of):
SUPERUSERrole (sufficient for everything), ORCREATEprivilege on database +CREATEprivilege on public schema- If
pgcryptois not installed: also needsSUPERUSERorCREATEDB
installer.isInstalled(): Promise<boolean>
Returns true if the eql_v2 schema exists in the database.
installer.getInstalledVersion(): Promise<string | null>
Returns the installed EQL version string, 'unknown' if schema exists but no version metadata, or null if not installed.
installer.install(options?): Promise<void>
Executes the EQL install SQL in a transaction.
await installer.install({
excludeOperatorFamily?: boolean // Skip operator family creation
supabase?: boolean // Use Supabase-compatible install + grant roles
latest?: boolean // Fetch latest from GitHub instead of bundled
})
Full programmatic example
import { EQLInstaller, loadStashConfig } from '@cipherstash/stack-forge'
const config = await loadStashConfig()
const installer = new EQLInstaller({ databaseUrl: config.databaseUrl })
// Check permissions first
const permissions = await installer.checkPermissions()
if (!permissions.ok) {
console.error('Missing permissions:', permissions.missing)
process.exit(1)
}
// Install if not already present
if (await installer.isInstalled()) {
const version = await installer.getInstalledVersion()
console.log(`EQL already installed (version: ${version})`)
} else {
await installer.install()
console.log('EQL installed successfully')
}
Requirements
- Node.js >= 22
- PostgreSQL database with sufficient permissions (see
checkPermissions()) - A
stash.config.tsfile with a validdatabaseUrl - Peer dependency:
@cipherstash/stack>= 0.6.0
Common issues
Permission errors during install
The database role needs CREATE privileges on the database and public schema, or SUPERUSER. Run checkPermissions() or check the CLI output for details on what's missing.
Config not found
stash.config.ts must be in the project root or a parent directory. The file must export default defineConfig(...).
Supabase environments
Always use --supabase (or supabase: true programmatically) when targeting Supabase. This uses a compatible install script and grants permissions to anon, authenticated, and service_role roles.
Operator families and ORDER BY
When EQL is installed with --supabase or --exclude-operator-family, PostgreSQL operator families are not created. This means ORDER BY on encrypted columns is not currently supported — regardless of the client or ORM used (Drizzle, Supabase JS SDK, raw SQL, etc.).
Sort application-side after decrypting the results as a workaround.
Operator family support for Supabase is being developed with the Supabase and CipherStash teams and will be available in a future release. This limitation applies to any database environment where operator families are not installed.