meta-issue-creation
How an agent files a GitHub issue on cipherstash repos — required structure (Background / Problem / Proposal), dumbed-down…
Integrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas. Covers item and bulk encryption, legacy v2 reads, HMAC query attributes, nested objects, audit logging, and the __source/__hmac storage convention.
$ npx -y skills add cipherstash/stack --skill stash-dynamodb --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/stash-dynamodbContext preview
The summary Claude sees to decide when to auto-load this skill.
Integrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas. Covers item and bulk encryption, legacy v2 reads, HMAC query attributes, nested objects, audit logging, and the __source/__hmac storage convention.
name: stash-dynamodb description: Integrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas. Covers item and bulk encryption, legacy v2 reads, HMAC query attributes, nested objects, audit logging, and the __source/__hmac storage convention.
Guide for integrating CipherStash field-level encryption with Amazon DynamoDB using `@cipherstash/stack/dynamodb`. The helper encrypts items before writing to DynamoDB and decrypts them after reading - it does not wrap the AWS SDK, so you keep full control of your DynamoDB operations.
npm install @cipherstash/stack @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
> **Version note:** `npx stash init` is the preferred install path — it pins > every `@cipherstash/*` package to the versions matching your CLI release. > If you install manually as above, verify what actually resolved > (`node -p "require('@cipherstash/stack/package.json').version"`): bare > dist-tag installs can lag behind a release, and `stash init` will warn on > the version skew.
CipherStash encrypts each attribute into two DynamoDB attributes:
| Original Attribute | Stored As | Purpose | |---|---|---| | `email` | `email__source` | Encrypted ciphertext | | `email` | `email__hmac` | HMAC — written whenever the domain produces an equality term, usable only for equality lookups (see the domain table below) |
Non-encrypted attributes pass through unchanged. On decryption, the `__source` and `__hmac` attributes are recombined back into the original attribute name with the plaintext value.
**Only equality is usable on DynamoDB.** Ordering terms and free-text bloom filters have no DynamoDB query surface, so they are not stored. A column in an ordering or free-text domain still encrypts and decrypts correctly — it just cannot back a key condition.
Schema authoring and every write are EQL v3-only. Existing v2 DynamoDB items remain readable by passing the same v3 table descriptor plus `{ storedEqlVersion: 2 }` to `decryptModel` or `bulkDecryptModels`. Both entries serve legacy reads — the default `@cipherstash/stack` one and `@cipherstash/stack/wasm-inline`.
There is no infrastructure migration between the versions — DynamoDB has no EQL extension to install and no schema to alter — and there is no automatic *data* migration either. To fully move a table to v3, re-encrypt every item with the v3 schema.
The client must be built for the table. Build the client with the same v3 table you hand to `encryptedDynamoDB` — `Encryption({ schemas: [users] })` returns the typed v3 client for a concrete v3 schema set. Passing a v3 table to a client that never registered it (a client built for a different schema set) throws a clear error naming the table on the first operation, instead of failing later with an opaque FFI deserialization error.
DynamoDB items are natively nested. Declare encrypted leaves as flat dotted paths; the item keeps its nested shape and unlisted siblings stay plaintext:
const users = encryptedTable("users", {
"profile.ssn": types.TextEq("profile.ssn"),
"profile.note": types.Text("profile.note"),
}){ "pk": "u#1",
"profile": {
"ssn__source": "<ciphertext>",
"ssn__hmac": "<hmac>", // equality term — FilterExpression only, not a key condition
"note__source": "<ciphertext>",
"city": "Sydney" // not in schema, stays plaintext
} }> A nested equality term like `profile.ssn__hmac` lives *inside* the `profile` > map, so it can only be matched with a `FilterExpression` — DynamoDB key > conditions and secondary-index keys must be top-level scalar attributes. If you > need the HMAC to back a key condition or GSI, declare the field as a top-level > column (`ssn: types.TextEq("ssn")`) so it is stored as a top-level `ssn__hmac`.
> The dotted string is the *property key* as well as the column name — the model > is matched by dotted path, so `{ profile: { ssn } }` resolves correctly.
To encrypt a whole subtree as one value instead of per-leaf, use `types.Json`, which stores it as a single ste_vec attribute (`profile__source`).
> **Arrays are not descended into.** The adapter splits encrypted leaves inside > nested *objects* only. A value inside an array is stored whole — it is not > split into `__source`/`__hmac`, so its ciphertext still decrypts on read but > can never back a key condition, and it does not appear in the `__source`/`__hmac` > attribute layout above. A DynamoDB key condition cannot target an array element > in any case. To encrypt list data, either promote the searched field to a > top-level column, or wrap the subtree in a single `types.Json` column.
DynamoDB encryption is **single-deploy**. There is no rollout/cutover split — unlike the Postgres path, DynamoDB has no row-level rename swap and no shared-state proxy. The application owns every write, so adding encryption is an application-side change that ships in one PR:
1. Declare the encrypted schema (see Setup below). 2. Build an `encryptedDynamoDB` helper and call `encryptModel` / `decryptModel` at your write and read sites. 3. Ship the change.
For tables with **existing populated items**, the `__source` and `__hmac` attributes are added by the next write that touches each item. If you need every existing item encrypted at once (e.g. because a query uses `email__hmac` and would miss legacy items), run a one-shot script that reads every item,
Searchable, application-level encryption for building privacy-first apps
How an agent files a GitHub issue on cipherstash repos — required structure (Background / Problem / Proposal), dumbed-down…
How an agent authors branches, commits, and pull requests on cipherstash/stack — naming, signed commits, the…
How a CipherStash client authenticates — the canonical skill for credentials, auth strategies, and lock context. Covers the…
Drive CipherStash setup and encryption migrations through the `stash` CLI — `init`, `plan`, `impl`, `status`, `auth login`, `eql…
Deploy a CipherStash encryption rollout to a live environment without losing data — the multi-deploy ladder (schema-add +…
Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack-drizzle (EQL v3). Covers the types.* encrypted column…