|
| 1 | +# @cipherstash/stack-forge |
| 2 | + |
| 3 | +Dev-time CLI and library for managing [CipherStash EQL](https://github.com/cipherstash/encrypt-query-language) (Encrypted Query Language) in your PostgreSQL database. |
| 4 | + |
| 5 | +[](https://www.npmjs.com/package/@cipherstash/stack-forge) |
| 6 | +[](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md) |
| 7 | +[](https://www.typescriptlang.org/) |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Why stack-forge? |
| 12 | + |
| 13 | +`@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, and managing schema lifecycle. |
| 14 | + |
| 15 | +Think of it like Prisma or Drizzle Kit — a companion CLI that sets up the database while the main SDK handles runtime operations. |
| 16 | + |
| 17 | +## Install |
| 18 | + |
| 19 | +```bash |
| 20 | +npm install -D @cipherstash/stack-forge |
| 21 | +``` |
| 22 | + |
| 23 | +Or with your preferred package manager: |
| 24 | + |
| 25 | +```bash |
| 26 | +pnpm add -D @cipherstash/stack-forge |
| 27 | +yarn add -D @cipherstash/stack-forge |
| 28 | +bun add -D @cipherstash/stack-forge |
| 29 | +``` |
| 30 | + |
| 31 | +## Quick Start |
| 32 | + |
| 33 | +You can install EQL in two ways: **direct install** (connects to the DB and runs the SQL) or **Drizzle migration** (generates a migration file; you run `drizzle-kit migrate` yourself). The steps below use the direct install path. |
| 34 | + |
| 35 | +### 1. Create a config file |
| 36 | + |
| 37 | +Create `stash.config.ts` in your project root: |
| 38 | + |
| 39 | +```typescript |
| 40 | +import { defineConfig } from '@cipherstash/stack-forge' |
| 41 | + |
| 42 | +export default defineConfig({ |
| 43 | + databaseUrl: process.env.DATABASE_URL!, |
| 44 | +}) |
| 45 | +``` |
| 46 | + |
| 47 | +### 2. Add a `.env` file |
| 48 | + |
| 49 | +```env |
| 50 | +DATABASE_URL=postgresql://user:password@localhost:5432/mydb |
| 51 | +``` |
| 52 | + |
| 53 | +### 3. Install EQL |
| 54 | + |
| 55 | +```bash |
| 56 | +npx stash-forge install |
| 57 | +``` |
| 58 | + |
| 59 | +That's it. EQL is now installed in your database. |
| 60 | + |
| 61 | +If your encryption client lives elsewhere, set `client` in `stash.config.ts` (e.g. `client: './lib/encryption.ts'`). That path is used by `stash-forge push`. |
| 62 | + |
| 63 | +**Using Drizzle?** To install EQL via your migration pipeline instead, run `npx stash-forge install --drizzle`, then `npx drizzle-kit migrate`. See [install --drizzle](#install---drizzle) below. |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## Configuration |
| 68 | + |
| 69 | +The `stash.config.ts` file is the single source of truth for stack-forge. It uses the `defineConfig` helper for type safety. |
| 70 | + |
| 71 | +```typescript |
| 72 | +import { defineConfig } from '@cipherstash/stack-forge' |
| 73 | + |
| 74 | +export default defineConfig({ |
| 75 | + // Required: PostgreSQL connection string |
| 76 | + databaseUrl: process.env.DATABASE_URL!, |
| 77 | + |
| 78 | + // Optional: path to your encryption client (default: './src/encryption/index.ts') |
| 79 | + // Used by `stash-forge push` to load the encryption schema |
| 80 | + client: './src/encryption/index.ts', |
| 81 | + |
| 82 | + // Optional: CipherStash workspace and credentials (for future schema sync) |
| 83 | + workspaceId: process.env.CS_WORKSPACE_ID, |
| 84 | + clientAccessKey: process.env.CS_CLIENT_ACCESS_KEY, |
| 85 | +}) |
| 86 | +``` |
| 87 | + |
| 88 | +| Option | Required | Description | |
| 89 | +|--------|----------|-------------| |
| 90 | +| `databaseUrl` | Yes | PostgreSQL connection string | |
| 91 | +| `client` | No | Path to encryption client file (default: `'./src/encryption/index.ts'`). Used by `push` to load the encryption schema. | |
| 92 | +| `workspaceId` | No | CipherStash workspace ID | |
| 93 | +| `clientAccessKey` | No | CipherStash client access key | |
| 94 | + |
| 95 | +The CLI automatically loads `.env` files before evaluating the config, so `process.env` references work out of the box. |
| 96 | + |
| 97 | +The config file is resolved by walking up from the current working directory, similar to how `tsconfig.json` resolution works. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## CLI Reference |
| 102 | + |
| 103 | +``` |
| 104 | +stash-forge <command> [options] |
| 105 | +``` |
| 106 | + |
| 107 | +### `install` |
| 108 | + |
| 109 | +Install the CipherStash EQL extensions into your database. |
| 110 | + |
| 111 | +```bash |
| 112 | +npx stash-forge install [options] |
| 113 | +``` |
| 114 | + |
| 115 | +| Option | Description | |
| 116 | +|--------|-------------| |
| 117 | +| `--dry-run` | Show what would happen without making changes | |
| 118 | +| `--force` | Reinstall even if EQL is already installed | |
| 119 | +| `--supabase` | Use Supabase-compatible install (excludes operator families + grants Supabase roles) | |
| 120 | +| `--exclude-operator-family` | Skip operator family creation (for non-superuser database roles) | |
| 121 | +| `--drizzle` | Generate a Drizzle migration instead of direct install | |
| 122 | +| `--name <value>` | Migration name when using `--drizzle` (default: `install-eql`) | |
| 123 | +| `--out <value>` | Drizzle output directory when using `--drizzle` (default: `drizzle`) | |
| 124 | + |
| 125 | +**Standard install:** |
| 126 | + |
| 127 | +```bash |
| 128 | +npx stash-forge install |
| 129 | +``` |
| 130 | + |
| 131 | +**Supabase install:** |
| 132 | + |
| 133 | +```bash |
| 134 | +npx stash-forge install --supabase |
| 135 | +``` |
| 136 | + |
| 137 | +The `--supabase` flag: |
| 138 | +- Downloads the Supabase-specific SQL variant (no `CREATE OPERATOR FAMILY`) |
| 139 | +- Grants `USAGE`, table, routine, and sequence permissions on the `eql_v2` schema to `anon`, `authenticated`, and `service_role` |
| 140 | + |
| 141 | +**Preview changes first:** |
| 142 | + |
| 143 | +```bash |
| 144 | +npx stash-forge install --dry-run |
| 145 | +``` |
| 146 | + |
| 147 | +#### `install --drizzle` |
| 148 | + |
| 149 | +If you use [Drizzle ORM](https://orm.drizzle.team/) 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. |
| 150 | + |
| 151 | +```bash |
| 152 | +npx stash-forge install --drizzle |
| 153 | +npx drizzle-kit migrate |
| 154 | +``` |
| 155 | + |
| 156 | +**How it works:** |
| 157 | + |
| 158 | +1. Runs `drizzle-kit generate --custom --name=<name>` to create an empty migration. |
| 159 | +2. Downloads the EQL install script from the [EQL GitHub releases](https://github.com/cipherstash/encrypt-query-language/releases/latest). |
| 160 | +3. Writes the EQL SQL into the generated migration file. |
| 161 | + |
| 162 | +With a custom migration name or output directory: |
| 163 | + |
| 164 | +```bash |
| 165 | +npx stash-forge install --drizzle --name setup-eql --out ./migrations |
| 166 | +npx drizzle-kit migrate |
| 167 | +``` |
| 168 | + |
| 169 | +You 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`). |
| 170 | + |
| 171 | +### `push` |
| 172 | + |
| 173 | +Load your encryption schema from the file specified by `client` in `stash.config.ts` and apply it to the database (or preview with `--dry-run`). |
| 174 | + |
| 175 | +```bash |
| 176 | +npx stash-forge push [options] |
| 177 | +``` |
| 178 | + |
| 179 | +| Option | Description | |
| 180 | +|--------|-------------| |
| 181 | +| `--dry-run` | Load and validate the schema, then print it as JSON. No database changes. | |
| 182 | + |
| 183 | +**Push schema to the database:** |
| 184 | + |
| 185 | +```bash |
| 186 | +npx stash-forge push |
| 187 | +``` |
| 188 | + |
| 189 | +This connects to Postgres, marks any existing rows in `eql_v2_configuration` as `inactive`, and inserts the current encrypt config as a new row with state `active`. Your runtime encryption (e.g. `@cipherstash/stack`) reads the active configuration from this table. |
| 190 | + |
| 191 | +**Preview your encryption schema without writing to the database:** |
| 192 | + |
| 193 | +```bash |
| 194 | +npx stash-forge push --dry-run |
| 195 | +``` |
| 196 | + |
| 197 | +### Permission Pre-checks (install) |
| 198 | + |
| 199 | +Before installing, `stash-forge` verifies that the connected database role has the required permissions: |
| 200 | + |
| 201 | +- `CREATE` on the database (for `CREATE SCHEMA` and `CREATE EXTENSION`) |
| 202 | +- `CREATE` on the `public` schema (for `CREATE TYPE public.eql_v2_encrypted`) |
| 203 | +- `SUPERUSER` or extension owner (for `CREATE EXTENSION pgcrypto`, if not already installed) |
| 204 | + |
| 205 | +If permissions are insufficient, the CLI exits with a clear message listing what's missing. |
| 206 | + |
| 207 | +### Planned Commands |
| 208 | + |
| 209 | +The following commands are defined but not yet implemented: |
| 210 | + |
| 211 | +| Command | Description | |
| 212 | +|---------|-------------| |
| 213 | +| `init` | Initialize CipherStash Forge in your project | |
| 214 | +| `migrate` | Run pending encrypt config migrations | |
| 215 | +| `status` | Show EQL installation status | |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## Programmatic API |
| 220 | + |
| 221 | +You can also use stack-forge as a library: |
| 222 | + |
| 223 | +```typescript |
| 224 | +import { EQLInstaller, loadStashConfig } from '@cipherstash/stack-forge' |
| 225 | + |
| 226 | +// Load config from stash.config.ts |
| 227 | +const config = await loadStashConfig() |
| 228 | + |
| 229 | +// Create an installer |
| 230 | +const installer = new EQLInstaller({ |
| 231 | + databaseUrl: config.databaseUrl, |
| 232 | +}) |
| 233 | + |
| 234 | +// Check permissions before installing |
| 235 | +const permissions = await installer.checkPermissions() |
| 236 | +if (!permissions.ok) { |
| 237 | + console.error('Missing permissions:', permissions.missing) |
| 238 | + process.exit(1) |
| 239 | +} |
| 240 | + |
| 241 | +// Check if already installed |
| 242 | +if (await installer.isInstalled()) { |
| 243 | + console.log('EQL is already installed') |
| 244 | +} else { |
| 245 | + await installer.install() |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +### `EQLInstaller` |
| 250 | + |
| 251 | +| Method | Returns | Description | |
| 252 | +|--------|---------|-------------| |
| 253 | +| `checkPermissions()` | `Promise<PermissionCheckResult>` | Check if the database role has required permissions | |
| 254 | +| `isInstalled()` | `Promise<boolean>` | Check if the `eql_v2` schema exists | |
| 255 | +| `getInstalledVersion()` | `Promise<string \| null>` | Get the installed EQL version (or `null`) | |
| 256 | +| `install(options?)` | `Promise<void>` | Download and execute the EQL install SQL in a transaction | |
| 257 | + |
| 258 | +#### Install Options |
| 259 | + |
| 260 | +```typescript |
| 261 | +await installer.install({ |
| 262 | + excludeOperatorFamily: true, // Skip CREATE OPERATOR FAMILY |
| 263 | + supabase: true, // Supabase mode (implies excludeOperatorFamily + grants roles) |
| 264 | +}) |
| 265 | +``` |
| 266 | + |
| 267 | +### `defineConfig` |
| 268 | + |
| 269 | +Type-safe identity function for `stash.config.ts`: |
| 270 | + |
| 271 | +```typescript |
| 272 | +import { defineConfig } from '@cipherstash/stack-forge' |
| 273 | + |
| 274 | +export default defineConfig({ |
| 275 | + databaseUrl: process.env.DATABASE_URL!, |
| 276 | +}) |
| 277 | +``` |
| 278 | + |
| 279 | +### `loadStashConfig` |
| 280 | + |
| 281 | +Finds and loads the nearest `stash.config.ts`, validates it with Zod, applies defaults (e.g. `client`), and returns the typed config: |
| 282 | + |
| 283 | +```typescript |
| 284 | +import { loadStashConfig } from '@cipherstash/stack-forge' |
| 285 | + |
| 286 | +const config = await loadStashConfig() |
| 287 | +// config.databaseUrl — guaranteed to be a non-empty string |
| 288 | +// config.client — path to encryption client (default: './src/encryption/index.ts') |
| 289 | +``` |
0 commit comments