DB Sculptor
EliasOulkadi/shokunin
Design database schemas with Prisma/Drizzle, PostgreSQL index strategy (B-tree, GIN, GiST, BRIN, Hash), query optimization (EXPLAIN ANALYZE), migration safety (expand/contract, zero-downtime), and…
Integrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas.
$ npx skills add cipherstash/stack --skill stash-dynamodb -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install cipherstash/stack stash-dynamodb --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/stash-dynamodb .claude/skills/stash-dynamodb && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "stash-dynamodb" agent skill from https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodb into .claude/skills/stash-dynamodb/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "stash-dynamodb", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodbType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add cipherstash/stack --skill stash-dynamodb -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install cipherstash/stack stash-dynamodb --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/stash-dynamodb .agents/skills/stash-dynamodb && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "stash-dynamodb" agent skill from https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodb into .agents/skills/stash-dynamodb/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "stash-dynamodb", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cipherstash/stack --skill stash-dynamodb -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install cipherstash/stack stash-dynamodb --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/stash-dynamodb .cursor/skills/stash-dynamodb && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "stash-dynamodb" agent skill from https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodb into .cursor/skills/stash-dynamodb/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "stash-dynamodb", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/cipherstash/stack.git --path skills/stash-dynamodb--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add cipherstash/stack --skill stash-dynamodb -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install cipherstash/stack stash-dynamodb --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/stash-dynamodb .gemini/skills/stash-dynamodb && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "stash-dynamodb" agent skill from https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodb into .gemini/skills/stash-dynamodb/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "stash-dynamodb", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install cipherstash/stack stash-dynamodbInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add cipherstash/stack --skill stash-dynamodb -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/stash-dynamodb .github/skills/stash-dynamodb && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "stash-dynamodb" agent skill from https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodb into .github/skills/stash-dynamodb/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "stash-dynamodb", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cipherstash/stack --skill stash-dynamodb -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install cipherstash/stack stash-dynamodb --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cipherstash/stack.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/stash-dynamodb .opencode/skills/stash-dynamodb && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "stash-dynamodb" agent skill from https://github.com/cipherstash/stack/tree/main/skills/stash-dynamodb into .opencode/skills/stash-dynamodb/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "stash-dynamodb", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
stash-dynamodbIntegrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas.
Stash Dynamodb is an agent skill from cipherstash/stack. 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.
Its SKILL.md is about 5.7k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Databases, covering NoSQL databases. It works with Amazon DynamoDB, PostgreSQL and Prisma. The repository describes itself as: Searchable, application-level encryption for building privacy-first apps. The licence is MIT.
2 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 415b62c. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
npmnpxnodepnpmyarnFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npm, npx, pnpm and yarn, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Stash Dynamodb loads about 5.7k tokens when it runs. Until then it costs about 68 tokens; SKILL.md has 1,845 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from cipherstash/stack at commit 415b62c, republished under its MIT licence (© cipherstash). 1,845 words, ~5,717 tokens.
.claude/skills/stash-dynamodb/SKILL.md (or your agent's skills folder).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-dynamodbVersion note:
npx stash initis 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, andstash initwill 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__hmaclives inside theprofilemap, so it can only be matched with aFilterExpression— 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-levelssn__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/__hmacattribute 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 singletypes.Jsoncolumn.
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:
encryptedDynamoDB helper and call encryptModel / decryptModel at your write and read sites.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, calls encryptModel, and writes it back. Idempotent: re-running an already-encrypted item is a no-op as long as the schema hasn't changed.
Where am I? Run
stash status(orbunx/pnpm dlx/yarn dlxper your runner) for a project-wide view across both Postgres and DynamoDB integrations. DynamoDB columns surface in the quest log as already-complete since there is no staged lifecycle to track.
Each types.* factory is a concrete domain with fixed query capabilities. There are no chainable index methods — the type is the capability.
import { encryptedTable, types } from "@cipherstash/stack/v3"
const users = encryptedTable("users", {
email: types.TextEq("email"), // equality -> queryable via email__hmac
name: types.Text("name"), // storage only
phone: types.Text("phone"), // storage only
age: types.IntegerOrd("age"), // decryptable, NOT queryable on DynamoDB
metadata: types.Json("metadata"), // JSON document
})Which domains give you a __hmac attribute you can query on:
| Domain family | __hmac written? | Notes |
|---|---|---|
types.TextEq, IntegerEq, BigintEq, DateEq, TimestampEq, NumericEq, RealEq, DoubleEq, SmallintEq | Yes | The equality domains — use these for anything you query |
types.TextOrd, TextOrdOre, TextSearch | Yes | Text equality is HMAC-based even on ordering/search domains |
types.IntegerOrd, DateOrd, TimestampOrd, and the other non-text *Ord/*OrdOre | No | Equality resolves through an ordering term in Postgres, which DynamoDB cannot use |
types.Text, Integer, Boolean, and the other bare domains | No | Storage only |
types.Json | No | Index terms live inside the ste_vec array; not splittable into an attribute |
Rule of thumb: if an attribute will appear in a
KeyConditionExpression, declare it with an*Eqdomain.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"
import { Encryption } from "@cipherstash/stack"
import { encryptedDynamoDB } from "@cipherstash/stack/dynamodb"
const dynamoClient = new DynamoDBClient({ region: "us-east-1" })
const docClient = DynamoDBDocumentClient.from(dynamoClient)
const encryptionClient = await Encryption({ schemas: [users] })
const dynamo = encryptedDynamoDB({ encryptionClient })Audit metadata on decrypt works.
decryptModel/bulkDecryptModelsare audit-chainable —dynamo.decryptModel(item, table).audit({ metadata })forwards the metadata to ZeroKMS on the default@cipherstash/stackentry. The@cipherstash/stack/wasm-inlineclient has no chainable operations, so audit metadata is dropped there.
Use the current v3 table descriptor and select the stored wire version on the read. No v2 builder or v2-configured client is needed.
Works on both entries. Schema authoring is EQL v3-only everywhere, but the
read itself is not: the legacy path reconstructs the v2 envelope around the
current v3 table, and decrypt accepts either wire generation. So Deno, Bun,
Workers and Supabase Edge Functions can read legacy items through
@cipherstash/stack/wasm-inline too.
const decrypted = await dynamo.decryptModel(
storedV2Item,
users,
{ storedEqlVersion: 2 },
)const dynamo = encryptedDynamoDB({
encryptionClient,
options: {
logger: {
error: (message, error) => console.error(`[DynamoDB] ${message}`, error),
},
errorHandler: (error) => {
// Send to monitoring, etc.
console.error(`[${error.code}] ${error.message}`)
},
},
})import { PutCommand } from "@aws-sdk/lib-dynamodb"
const user = {
pk: "user#1",
email: "alice@example.com", // will be encrypted
name: "Alice Smith", // will be encrypted
role: "admin", // not in schema, passes through
}
const result = await dynamo.encryptModel(user, users)
if (result.failure) {
console.error("Encryption failed:", result.failure.message)
} else {
await docClient.send(new PutCommand({
TableName: "Users",
Item: result.data,
// result.data looks like:
// {
// pk: "user#1",
// email__source: "<ciphertext>",
// email__hmac: "<hmac>",
// name__source: "<ciphertext>",
// role: "admin",
// }
}))
}import { BatchWriteCommand } from "@aws-sdk/lib-dynamodb"
const items = [
{ pk: "user#1", email: "alice@example.com", name: "Alice" },
{ pk: "user#2", email: "bob@example.com", name: "Bob" },
]
const result = await dynamo.bulkEncryptModels(items, users)
if (!result.failure) {
await docClient.send(new BatchWriteCommand({
RequestItems: {
Users: result.data.map(item => ({
PutRequest: { Item: item },
})),
},
}))
}import { GetCommand } from "@aws-sdk/lib-dynamodb"
const getResult = await docClient.send(new GetCommand({
TableName: "Users",
Key: { pk: "user#1" },
}))
const result = await dynamo.decryptModel(getResult.Item, users)
if (!result.failure) {
console.log(result.data)
// { pk: "user#1", email: "alice@example.com", name: "Alice Smith", role: "admin" }
}import { BatchGetCommand } from "@aws-sdk/lib-dynamodb"
const batchResult = await docClient.send(new BatchGetCommand({
RequestItems: {
Users: {
Keys: [{ pk: "user#1" }, { pk: "user#2" }],
},
},
}))
const result = await dynamo.bulkDecryptModels(
batchResult.Responses?.Users ?? [],
users,
)
if (!result.failure) {
for (const user of result.data) {
console.log(user.email) // plaintext
}
}DynamoDB queries use key conditions, so you need to encrypt the search value into its HMAC form. Use encryptionClient.encryptQuery() to get the HMAC, then use it in your key condition.
When an encrypted attribute is the partition key (e.g., email__hmac):
import { QueryCommand } from "@aws-sdk/lib-dynamodb"
// 1. Encrypt the search value to get the HMAC.
// On an EQL v3 equality domain this mints the bare term — `{ v, i, hm }`
// with no ciphertext — so `hm` is used directly.
const queryResult = await encryptionClient.encryptQuery("alice@example.com", {
table: users,
column: users.email,
})
if (queryResult.failure) {
throw new Error(`Query encryption failed: ${queryResult.failure.message}`)
}
const emailHmac = queryResult.data.hm
// 2. Use the HMAC in a DynamoDB query
const result = await docClient.send(new QueryCommand({
TableName: "Users",
KeyConditionExpression: "email__hmac = :email",
ExpressionAttributeValues: {
":email": emailHmac,
},
}))
// 3. Decrypt the results
const decrypted = await dynamo.bulkDecryptModels(result.Items ?? [], users)When an encrypted attribute is the sort key:
const result = await docClient.send(new GetCommand({
TableName: "Users",
Key: {
pk: "org#1", // partition key (plain)
email__hmac: emailHmac, // sort key (encrypted HMAC)
},
}))
const decrypted = await dynamo.decryptModel(result.Item, users)When querying a Global Secondary Index where the GSI key is an encrypted HMAC:
const result = await docClient.send(new QueryCommand({
TableName: "Users",
IndexName: "EmailIndex",
KeyConditionExpression: "email__hmac = :email",
ExpressionAttributeValues: {
":email": emailHmac,
},
Limit: 1,
}))
if (result.Items?.length) {
const decrypted = await dynamo.decryptModel(result.Items[0], users)
}All operations support .audit() chaining for audit metadata:
const result = await dynamo
.encryptModel(user, users)
.audit({
metadata: {
sub: "user-id-123",
action: "user_registration",
timestamp: new Date().toISOString(),
},
})For each encrypted field with an equality index, two attributes are stored:
{field}__source - The encrypted ciphertext (binary/string){field}__hmac - Deterministic HMAC for equality lookupsFields without equality capability only get __source (no HMAC, so they can't be queried) — that means EQL v2 columns without .equality(), and EQL v3 columns outside the domains listed in the table above.
| Pattern | Partition Key | Sort Key | Use Case |
|---|---|---|---|
| Plain PK | pk (plain) | - | Standard lookup by ID |
| Encrypted PK | email__hmac | - | Lookup by encrypted attribute |
| Encrypted SK | pk (plain) | email__hmac | Composite key with encrypted sort |
| GSI on HMAC | pk (plain) | - | Query by encrypted attribute via GSI with email__hmac as GSI PK |
__hmac attributes (exact match only)attribute_exists(email__source) / attribute_not_exists(email__source) in condition expressionsBETWEEN, <, > on __source)begins_with, contains on __source)__source values are encrypted binary - only equality via __hmac is supportedAll operations return Result<T, EncryptedDynamoDBError> with either data or failure:
const result = await dynamo.encryptModel(user, users)
if (result.failure) {
console.error(result.failure.message)
console.error(result.failure.code)
// code: ProtectErrorCode | "DYNAMODB_ENCRYPTION_ERROR"
console.error(result.failure.details)
}encryptedDynamoDB(config)import { encryptedDynamoDB } from "@cipherstash/stack/dynamodb"
const dynamo = encryptedDynamoDB({
// From Encryption(...)
encryptionClient,
options: { // optional
logger: { error: (message, error) => void },
errorHandler: (error) => void,
}
})All methods accept an EQL v3 table. Decrypt defaults to stored EQL v3 and can reconstruct a legacy v2 envelope when explicitly requested.
EQL v3 — the item is checked against the table's column domains, and the result is typed as the attribute map that is actually stored: a declared column email becomes email__source (plus email__hmac if its domain mints one), NOT email.
| Method | Signature | Resolves to |
|---|---|---|
encryptModel | (item, v3Table) | EncryptedAttributes<Table, T> |
bulkEncryptModels | (items, v3Table) | EncryptedAttributes<Table, T>[] |
decryptModel | (storedItem, v3Table, readOptions?) | DecryptedAttributes<Table, T> — __source folded back to the column, __hmac dropped |
bulkDecryptModels | (storedItems, v3Table, readOptions?) | DecryptedAttributes<Table, T>[] |
Let T be inferred from the argument; do not pass explicit type arguments on the v3 path.
For a stored v2 item, pass { storedEqlVersion: 2 } as readOptions. The table
is still the current v3 descriptor, which supplies table and column identity.
That descriptor must be one of the tables you passed to Encryption({ schemas }).
The adapter forwards it to the client to drive envelope and Date reconstruction,
and the client rejects a table it was not initialized with — so a legacy read of a
table your current schema no longer declares fails with decryptModel received a table this client was not initialized with. Keep the table declared for as long
as you still need to read its v2 rows.
| Method | Signature | Resolves to |
|---|---|---|
decryptModel | (item, v3Table, { storedEqlVersion: 2 }) | T |
bulkDecryptModels | (items, v3Table, { storedEqlVersion: 2 }) | T[] |
Grouped v2 fields. A v2 column inside a group was stored as
<group>.<leaf>__source while the v2 schema knew it only as <leaf>. On a
{ storedEqlVersion: 2 } read the leaf is matched inside the group, so carrying
the column forward as a plain top-level amount: types.TextEq('amount') reads
those rows correctly. You can also name it by its full path, keeping the
original DB name — 'details.amount': types.TextEq('amount'). Note the two
differ: the property is the dotted path, the argument is the v2 DB name. This
applies to v2 storage only; a v3 nested field uses the same dotted path for
both ('profile.ssn': types.TextEq('profile.ssn')).
Type reconstruction follows either spelling: a grouped v2 date / timestamp
column comes back as a Date, not an ISO string, whether you declare it as a
plain top-level placedAt: types.Date('placed_at') or as the full dotted path.
All operations are thenable (awaitable) and support .audit({ metadata }) chaining. On the default @cipherstash/stack entry the metadata forwards to ZeroKMS on every operation, encrypt and decrypt alike (see the Setup note). The @cipherstash/stack/wasm-inline client has no .audit() — its operations return a plain promise — so audit metadata is dropped there (logged at debug level). The operation itself still succeeds; only the audit record is lost. Use the native entry when audit trails matter.
Types exported from @cipherstash/stack/dynamodb: EncryptedDynamoDBInstance, EncryptedDynamoDBConfig, EncryptedDynamoDBError, AnyEncryptedTable, DynamoDBReadOptions, DynamoDBEncryptionClient, EncryptedAttributes, DecryptedAttributes, AuditConfig.
Use the encryption client directly (not the DynamoDB helper):
// EQL v3 — the domain fixes the query type, so no `queryType` is needed:
const result = await encryptionClient.encryptQuery(
"search-value",
{ table: users, column: users.email }
)
// encryptQuery returns a Result; check the failure branch before reading `data`.
// `data?.hm` would mask a failure (and a null-plaintext result) as `undefined`,
// producing a malformed key condition rather than a clear error.
if (result.failure) throw new Error(result.failure.message)
const hmac = result.data.hm // Use this in DynamoDB key conditions
// EQL v2 — pass queryType explicitly (`usersV2` is the legacy table declared
// in the v2 read section above; `users` is the v3 one and infers its queryType):
const v2Result = await encryptionClient.encryptQuery(
"search-value",
{ table: usersV2, column: usersV2.email, queryType: "equality" }
)
if (v2Result.failure) throw new Error(v2Result.failure.message)
const v2Hmac = v2Result.data.hmOn a
types.TextSearchcolumnencryptQueryreturnshmalongside ordering and bloom-filter terms regardless ofqueryType. Onlyhmis meaningful for DynamoDB — a free-text query cannot be expressed as a key condition.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocumentClient, PutCommand, GetCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"
import { Encryption } from "@cipherstash/stack"
import { encryptedTable, types } from "@cipherstash/stack/v3"
import { encryptedDynamoDB } from "@cipherstash/stack/dynamodb"
// Schema
const users = encryptedTable("users", {
email: types.TextEq("email"),
name: types.Text("name"),
})
// Clients
const dynamoClient = new DynamoDBClient({ region: "us-east-1" })
const docClient = DynamoDBDocumentClient.from(dynamoClient)
const encryptionClient = await Encryption({ schemas: [users] })
const dynamo = encryptedDynamoDB({ encryptionClient })
// Write
const user = { pk: "user#1", email: "alice@example.com", name: "Alice" }
const encResult = await dynamo.encryptModel(user, users)
if (!encResult.failure) {
await docClient.send(new PutCommand({ TableName: "Users", Item: encResult.data }))
}
// Read by primary key
const getResult = await docClient.send(new GetCommand({
TableName: "Users",
Key: { pk: "user#1" },
}))
const decResult = await dynamo.decryptModel(getResult.Item, users)
if (!decResult.failure) {
console.log(decResult.data.email) // "alice@example.com"
}
// Query by encrypted email (via HMAC)
const queryEnc = await encryptionClient.encryptQuery("alice@example.com", {
table: users,
column: users.email,
})
if (queryEnc.failure) throw new Error(queryEnc.failure.message)
const hmac = queryEnc.data.hm
const queryResult = await docClient.send(new QueryCommand({
TableName: "Users",
IndexName: "EmailIndex",
KeyConditionExpression: "email__hmac = :e",
ExpressionAttributeValues: { ":e": hmac },
}))
const decrypted = await dynamo.bulkDecryptModels(queryResult.Items ?? [], users)© cipherstash, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/stash-dynamodb of cipherstash/stack.
Open the folder on GitHubat commit 415b62c
Stash Dynamodb next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Stash Dynamodb this skillcipherstash/stack | 157 | — | ~5.7k | Automated safety check: Pass | MIT | |
| DB SculptorEliasOulkadi/shokunin | 114 | — | ~3.1k | Automated safety check: Notes | MIT | |
| Prisma Database Setupcurvenote/curvenote | 169 | 3 repos | ~1.4k | Automated safety check: Pass | MIT | |
| Database FundamentalsDanielPodolsky/ownyourcode | 290 | 1 repos | ~1.6k | Automated safety check: Pass | MIT | |
| Database Expertcin12211/orca-q | 223 | — | ~2.8k | Automated safety check: Pass | MIT | |
| Database Designerborghei/Claude-Skills | 874 | — | ~1.6k | Automated safety check: Pass | MIT |
EliasOulkadi/shokunin
Design database schemas with Prisma/Drizzle, PostgreSQL index strategy (B-tree, GIN, GiST, BRIN, Hash), query optimization (EXPLAIN ANALYZE), migration safety (expand/contract, zero-downtime), and…
curvenote/curvenote
Guides for configuring Prisma with different database providers (PostgreSQL, MySQL, SQLite, MongoDB, etc.).
DanielPodolsky/ownyourcode
Reviews schema design, SQL queries, ORM patterns. An agent skill from DanielPodolsky/ownyourcode.
cin12211/orca-q
Database performance optimization, schema design, query analysis, and connection management across PostgreSQL, MySQL, MongoDB, and SQLite with ORM integration.
borghei/Claude-Skills
Database design with schema analysis, index optimization, and migration generation for PostgreSQL, MySQL, MongoDB, and DynamoDB.
almeidazs/better-drizzle
Write, review, and debug code that uses better-drizzle, the typed repository layer over Drizzle ORM 1.x (better(db), client.users.findMany, paginate, cursor, upsertMany, relation include/connect…
cipherstash/stack
How an agent files a GitHub issue on cipherstash repos — required structure (Background / Problem / Proposal), dumbed-down wording rules, and pre-filing checks.
cipherstash/stack
How an agent authors branches, commits, and pull requests on cipherstash/stack — naming, signed commits, the changeset/skills/meta-file checklist, and PR body structure with dumbed-down wording.
cipherstash/stack
The ZeroKMS key model — keysets, clients, client keys, and the grant/revoke lifecycle.
cipherstash/stack
Deploy a CipherStash encryption rollout to a live environment without losing data — the multi-deploy ladder (schema-add + dual-write → backfill → read cutover → stop dual-writes → drop plaintext)…
cipherstash/stack
Supply-chain security controls for the @cipherstash/stack monorepo.
cipherstash/stack
Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack-drizzle (EQL v3).
Works with
Categories
Integrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas. Stash Dynamodb is an agent skill from cipherstash/stack. Integrate CipherStash encryption with Amazon DynamoDB using @cipherstash/stack/dynamodb and EQL v3 schemas.
Stash Dynamodb fits situations like: tasks that involve NoSQL databases.
Run `npx skills add cipherstash/stack --skill stash-dynamodb -a claude-code`. Or copy the skill folder (skills/stash-dynamodb in cipherstash/stack) into .claude/skills/stash-dynamodb in your project. Claude Code loads it when a task matches its description.
Run `npx skills add cipherstash/stack --skill stash-dynamodb -a codex`. Or copy the skill folder (skills/stash-dynamodb in cipherstash/stack) into .agents/skills/stash-dynamodb in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add cipherstash/stack --skill stash-dynamodb -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/stash-dynamodb, .gemini/skills/stash-dynamodb, .github/skills/stash-dynamodb and .opencode/skills/stash-dynamodb in your project.
Going by SKILL.md and its folder, Stash Dynamodb needs the command-line tools its instructions call (npm, npx, node, pnpm and yarn). Our summary lists: Node.js.
SKILL.md contains no URLs. Its commands use npm and npx, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Stash Dynamodb is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 5.7k tokens (SKILL.md is roughly 23k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Stash Dynamodb: DB Sculptor (EliasOulkadi/shokunin, 114 stars), Prisma Database Setup (curvenote/curvenote, 169 stars), Database Fundamentals (DanielPodolsky/ownyourcode, 290 stars) and Database Expert (cin12211/orca-q, 223 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
cipherstash (a GitHub organization) maintains it in cipherstash/stack, which has 157 GitHub stars. The repository holds 14 skills in this directory. The repository was last updated on October 7, 2026.
Source: cipherstash/stack on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.