@cipherstash/stack-forge
v0.4.0
Published
CipherStash Stack Forge
Keywords
Readme
@cipherstash/stack-forge
Dev-time CLI and library for managing CipherStash EQL (Encrypted Query Language) in your PostgreSQL database.
Why stack-forge?
@cipherstash/stack is the runtime encryption SDK — it should stay lean and free of heavy dependencies like pg. @cipherstash/stack-forge is a devDependency that handles database tooling: installing EQL extensions, checking permissions, validating schemas, and managing schema lifecycle.
Think of it like Prisma or Drizzle Kit — a companion CLI that sets up the database while the main SDK handles runtime operations.
Install
npm install -D @cipherstash/stack-forgeOr with your preferred package manager:
pnpm add -D @cipherstash/stack-forge
yarn add -D @cipherstash/stack-forge
bun add -D @cipherstash/stack-forgeQuick Start
First, initialize your project with the stash CLI (from @cipherstash/stack):
npx stash initThis generates your encryption schema and installs @cipherstash/stack-forge as a dev dependency.
Then set up your database and install EQL:
npx stash-forge setupThis will:
- Auto-detect your encryption client file (or ask for the path)
- Ask for your database URL
- Generate
stash.config.ts - Ask which Postgres provider you're using (Supabase, Neon, AWS RDS, etc.) to determine the right install flags
- Install EQL extensions in your database
That's it. EQL is now installed and your encryption schema is ready.
Manual setup
If you prefer to set things up manually:
1. Create a config file
Create stash.config.ts in your project root:
import { defineConfig } from '@cipherstash/stack-forge'
export default defineConfig({
databaseUrl: process.env.DATABASE_URL!,
client: './src/encryption/index.ts',
})2. Add a .env file
DATABASE_URL=postgresql://user:password@localhost:5432/mydb3. Install EQL
npx stash-forge installUsing Drizzle? To install EQL via your migration pipeline instead, run npx stash-forge install --drizzle, then npx drizzle-kit migrate. See install --drizzle below.
Configuration
The stash.config.ts file is the single source of truth for stack-forge. It uses the defineConfig helper for type safety.
import { defineConfig } from '@cipherstash/stack-forge'
export default defineConfig({
// Required: PostgreSQL connection string
databaseUrl: process.env.DATABASE_URL!,
// Optional: path to your encryption client (default: './src/encryption/index.ts')
// Used by `stash-forge push` and `stash-forge validate` to load the encryption schema
client: './src/encryption/index.ts',
})| Option | Required | Description |
|--------|----------|-------------|
| databaseUrl | Yes | PostgreSQL connection string |
| client | No | Path to encryption client file (default: './src/encryption/index.ts'). Used by push and validate to load the encryption schema. |
The CLI automatically loads .env files before evaluating the config, so process.env references work out of the box.
The config file is resolved by walking up from the current working directory, similar to how tsconfig.json resolution works.
CLI Reference
stash-forge <command> [options]setup
Configure your database and install EQL extensions. Run this after stash init has set up your encryption schema.
npx stash-forge setup [options]The wizard will:
- Auto-detect your encryption client file by scanning common locations (
./src/encryption/index.ts, etc.), then confirm with you or ask for the path if not found - Ask for your database URL (pre-fills from
DATABASE_URLenv var) - Generate
stash.config.tswith the database URL and client path - Ask which Postgres provider you're using to determine the right install flags:
- Supabase — uses
--supabase(no operator families + Supabase role grants) - Neon, Vercel Postgres, PlanetScale, Prisma Postgres — uses
--exclude-operator-family - AWS RDS, Other / Self-hosted — standard install
- Supabase — uses
- Install EQL extensions in your database
If --supabase is passed as a flag, the provider selection is skipped.
| Option | Description |
|--------|-------------|
| --force | Overwrite existing stash.config.ts and reinstall EQL |
| --dry-run | Show what would happen without making changes |
| --supabase | Skip provider selection and use Supabase-compatible install |
| --drizzle | Generate a Drizzle migration instead of direct install |
| --exclude-operator-family | Skip operator family creation |
| --latest | Fetch the latest EQL from GitHub instead of using the bundled version |
install
Install the CipherStash EQL extensions into your database. Uses bundled SQL by default for offline, deterministic installs.
npx stash-forge install [options]| Option | Description |
|--------|-------------|
| --dry-run | Show what would happen without making changes |
| --force | Reinstall even if EQL is already installed |
| --supabase | Use Supabase-compatible install (excludes operator families + grants Supabase roles) |
| --exclude-operator-family | Skip operator family creation (for non-superuser database roles) |
| --drizzle | Generate a Drizzle migration instead of direct install |
| --latest | Fetch the latest EQL from GitHub instead of using the bundled version |
| --name <value> | Migration name when using --drizzle (default: install-eql) |
| --out <value> | Drizzle output directory when using --drizzle (default: drizzle) |
Standard install:
npx stash-forge installSupabase install:
npx stash-forge install --supabaseThe --supabase flag:
- Uses the Supabase-specific SQL variant (no
CREATE OPERATOR FAMILY) - Grants
USAGE, table, routine, and sequence permissions on theeql_v2schema toanon,authenticated, andservice_role
Note: Without operator families,
ORDER BYon encrypted columns is not currently supported — regardless of the client or ORM used. Sort application-side after decrypting the results as a workaround. Operator family support for Supabase is being developed with the Supabase and CipherStash teams. This limitation also applies when using--exclude-operator-familyon any database.
Preview changes first:
npx stash-forge install --dry-runFetch the latest EQL from GitHub instead of using the bundled version:
npx stash-forge install --latestinstall --drizzle
If you use Drizzle ORM and want EQL installation as part of your migration history, use the --drizzle flag. It creates a Drizzle migration file containing the EQL install SQL, then you run your normal Drizzle migrations to apply it.
npx stash-forge install --drizzle
npx drizzle-kit migrateHow it works:
- Runs
drizzle-kit generate --custom --name=<name>to create an empty migration. - Loads the bundled EQL install SQL (or downloads from GitHub with
--latest). - Writes the EQL SQL into the generated migration file.
With a custom migration name or output directory:
npx stash-forge install --drizzle --name setup-eql --out ./migrations
npx drizzle-kit migrateYou need drizzle-kit installed in your project (npm install -D drizzle-kit). The --out directory must match your Drizzle config (e.g. drizzle.config.ts).
upgrade
Upgrade an existing EQL installation to the version bundled with the package (or the latest from GitHub).
npx stash-forge upgrade [options]| Option | Description |
|--------|-------------|
| --dry-run | Show what would happen without making changes |
| --supabase | Use Supabase-compatible upgrade |
| --exclude-operator-family | Skip operator family creation |
| --latest | Fetch the latest EQL from GitHub instead of using the bundled version |
The EQL install SQL is idempotent and safe to re-run. The upgrade command checks the current version, re-runs the install SQL, then reports the new version.
npx stash-forge upgradeIf EQL is not installed, the command suggests running stash-forge install instead.
validate
Validate your encryption schema for common misconfigurations.
npx stash-forge validate [options]| Option | Description |
|--------|-------------|
| --supabase | Check for Supabase-specific issues (e.g. ORDER BY without operator families) |
| --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") |
# Basic validation
npx stash-forge validate
# Validate with Supabase context
npx stash-forge validate --supabaseValidation is also automatically run before push — issues are logged as warnings but don't block the push.
The command exits with code 1 if there are errors (not for warnings or info).
push
Push your encryption schema to the database. This 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.
npx stash-forge push [options]| Option | 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 the current state of EQL in your database.
npx stash-forge statusReports:
- 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
Verify that the database URL in your config is valid and the database is reachable.
npx stash-forge test-connectionReports the database name, connected user/role, and PostgreSQL server version. Useful for debugging connection issues before running install or push.
Permission Pre-checks (install)
Before installing, stash-forge verifies that the connected database role has the required permissions:
CREATEon the database (forCREATE SCHEMAandCREATE EXTENSION)CREATEon thepublicschema (forCREATE TYPE public.eql_v2_encrypted)SUPERUSERor extension owner (forCREATE EXTENSION pgcrypto, if not already installed)
If permissions are insufficient, the CLI exits with a clear message listing what's missing.
Planned Commands
| Command | Description |
|---------|-------------|
| migrate | Run pending encrypt config migrations |
Bundled EQL SQL
The EQL install SQL is bundled with the package for offline, deterministic installs. Three variants are included:
| File | Used when |
|------|-----------|
| cipherstash-encrypt.sql | Default install |
| cipherstash-encrypt-supabase.sql | --supabase flag |
| cipherstash-encrypt-no-operator-family.sql | --exclude-operator-family flag |
The bundled SQL version is pinned to the package version. Use --latest to fetch the newest version from GitHub instead.
Programmatic API
You can also use stack-forge as a library:
import { EQLInstaller, loadStashConfig } from '@cipherstash/stack-forge'
// Load config from stash.config.ts
const config = await loadStashConfig()
// Create an installer
const installer = new EQLInstaller({
databaseUrl: config.databaseUrl,
})
// Check permissions before installing
const permissions = await installer.checkPermissions()
if (!permissions.ok) {
console.error('Missing permissions:', permissions.missing)
process.exit(1)
}
// Check if already installed
if (await installer.isInstalled()) {
console.log('EQL is already installed')
} else {
await installer.install()
}EQLInstaller
| Method | Returns | Description |
|--------|---------|-------------|
| checkPermissions() | Promise<PermissionCheckResult> | Check if the database role has required permissions |
| isInstalled() | Promise<boolean> | Check if the eql_v2 schema exists |
| getInstalledVersion() | Promise<string \| null> | Get the installed EQL version (or null) |
| install(options?) | Promise<void> | Execute the EQL install SQL in a transaction |
Install Options
await installer.install({
excludeOperatorFamily: true, // Skip CREATE OPERATOR FAMILY
supabase: true, // Supabase mode (implies excludeOperatorFamily + grants roles)
latest: true, // Fetch latest from GitHub instead of bundled
})loadBundledEqlSql
Load the bundled EQL install SQL as a string (useful for custom install workflows):
import { loadBundledEqlSql } from '@cipherstash/stack-forge'
const sql = loadBundledEqlSql() // standard
const sql = loadBundledEqlSql({ supabase: true }) // supabase variant
const sql = loadBundledEqlSql({ excludeOperatorFamily: true }) // no operator familydownloadEqlSql
Download the latest EQL install SQL from GitHub:
import { downloadEqlSql } from '@cipherstash/stack-forge'
const sql = await downloadEqlSql() // standard
const sql = await downloadEqlSql(true) // no operator family variantdefineConfig
Type-safe identity function for stash.config.ts:
import { defineConfig } from '@cipherstash/stack-forge'
export default defineConfig({
databaseUrl: process.env.DATABASE_URL!,
})loadStashConfig
Finds and loads the nearest stash.config.ts, validates it with Zod, applies defaults (e.g. client), and returns the typed config:
import { loadStashConfig } from '@cipherstash/stack-forge'
const config = await loadStashConfig()
// config.databaseUrl — guaranteed to be a non-empty string
// config.client — path to encryption client (default: './src/encryption/index.ts')