Use when defining DynamoDB entities, modeling indexes, or writing queries/mutations with the ElectroDB ORM. Covers entity schema, access patterns, CRUD operations, update expressions, and single-table design patterns.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add whimzyLive/nightshift-ai --skill electrodb --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Electrodb?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/whimzylive-electrodb)More formats (shields.io, HTML) on the badges page.
---
name: electrodb
description: Use when defining DynamoDB entities, modeling indexes, or writing queries/mutations with the ElectroDB ORM. Covers entity schema, access patterns, CRUD operations, update expressions, and single-table design patterns.
---
# ElectroDB
TypeScript-first DynamoDB ORM. Manages composite key composition, enforces attribute types, and generates safe DocumentClient params. No raw `DynamoDBClient` calls.
## Installation
```bash
npm install electrodb @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
```
## Entity Definition
```typescript
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { Entity } from "electrodb";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const Task = new Entity(
{
model: {
entity: "task", // item type namespace
service: "taskapp", // application namespace
version: "1",
},
attributes: {
taskId: { type: "string", required: true, default: () => crypto.randomUUID() },
projectId: { type: "string", required: true },
employeeId: { type: "string", required: true },
status: { type: ["pending", "active", "done"] as const, required: true },
description: { type: "string" },
ttl: { type: "number" }, // epoch seconds for DynamoDB TTL
createdAt: { type: "string", readOnly: true, default: () => new Date().toISOString() },
updatedAt: { type: "string", watch: "*", set: () => new Date().toISOString() },
},
indexes: {
// Table index (no `index` property = primary table index)
primary: {
pk: { field: "pk", composite: ["taskId"] },
sk: { field: "sk", composite: ["projectId"] },
},
// GSI — reference by logical name, not DynamoDB index name
byProject: {
index: "gsi1pk-gsi1sk-index",
pk: { field: "gsi1pk", composite: ["projectId"] },
sk: { field: "gsi1sk", composite: ["employeeId", "taskId"] },
},
},
},
{ table: process.env.TABLE_NAME!, client },
);
```
## Schema Quick Reference
### Attribute options
| Option | Description |
|--------|-------------|
| `type` | `"string"` `"number"` `"boolean"` `"list"` `"map"` `"set"` or enum array `["a","b"]` |
| `required` | Throw on put/create if missing |
| `default` | Static value or `() => value` factory |
| `readOnly` | Cannot be changed after creation |
| `watch: "*"` | Re-run `set` on every update (useful for `updatedAt`) |
| `validate` | Regex or `(val) => boolean` |
### model fields
| Field | Purpose |
|-------|---------|
| `model.entity` | Namespaces items on the table; prevents cross-entity key collisions |
| `model.service` | Groups entities into a Service for cross-entity queries |
| `model.version` | Bumping changes key format — migrate existing items first |
## Index Design
```typescript
indexes: {
// Primary (table index) — no `index` property
primary: {
pk: { field: "pk", composite: ["tenantId"] },
sk: { field: "sk", composite: ["entityId", "createdAt"] },
},
// GSI
byStatus: {
index: "gsi1pk-gsi1sk-index",
pk: { field: "gsi1pk", composite: ["tenantId", "status"] },
sk: { field: "gsi1sk", composite: ["createdAt"] },
},
// LSI (same pk field as table index)
byDate: {
index: "lsi1-index",
pk: { field: "pk", composite: ["tenantId"] },
sk: { field: "lsi1sk", composite: ["createdAt"] },
},
}
```
**Key composition rule:** composite array order is hierarchical — you can query on a prefix but not skip attributes. `["a","b","c"]` supports queries on `a` or `a+b` or `a+b+c`, never `b` alone.
## CRUD Operations
All operations are lazy — chain methods build the expression, then call `.go()` to execute or `.params()` to inspect the raw DocumentClient params.
### put (full overwrite)
```typescript
const { data } = await Task.put({
taskId: "t-001",
projectId: "proj-x",
employeeId: "emp-42",
status: "pending",
}).go();
```
### create (fails if item exists)
```typescript
await Task.create({ taskId, projectId, employeeId, status: "pending" }).go();
```
### get
```typescript
const { data } = await Task.get({ taskId: "t-001", projectId: "proj-x" }).go();
// data is null if not found
```
### delete
```typescript
await Task.delete({ taskId: "t-001", projectId: "proj-x" }).go();
```
### update
```typescript
// Chain operations
await Task.update({ taskId, projectId })
.set({ status: "active", description: "started" })
.go();
// data() for type-safe complex updates
await Task.update({ taskId, projectId })
.data((a, o) => {
o.set(a.status, "done");
o.remove(a.description);
o.add(a.retryCount, 1); // increment number
o.append(a.log, ["step complete"]); // append to list
})
.go();
```
### patch (update + fails if item missing)
```typescript
await Task.patch({ taskId, projectId }).set({ status: "done" }).go();
```
## Queries
```typescript
// By primary index — pk composite only
const { data } = await Task.query.primary({ taskId: "t-001" }).go();
// By GSI — filter sk with begins/between/gte/lte
const { data } = await Task.query.byProject({ projectId: "proj-x" })
.begins({ employeeId: "emp-" })
.go();
// Sort descending, limit, paginate
const { data, cursor } = await Task.query.byProject({ projectId: "proj-x" })
.go({ order: "desc", limit: 20 });
// Next page
const { data: page2 } = await Task.query.byProject({ projectId: "proj-x" })
.go({ cursor });
```
### Sort key operations
| Method | DynamoDB equivalent |
|--------|-------------------|
| `.begins({ sk: "val" })` | `begins_with` |
| `.between({ sk: "a" }, { sk: "z" })` | `BETWEEN` |
| `.gte({ sk: "val" })` | `>= val` |
| `.gt({ sk: "val" })` | `> val` |
| `.lte({ sk: "val" })` | `<= val` |
| `.lt({ sk: "val" })` | `< val` |
### Filter (post-query)
```typescript
const { data } = await Task.query.byProject({ projectId })
.where((attr, op) => op.eq(attr.status, "pending"))
.go();
// Multiple conditions
.where((attr, op) => `${op.eq(attr.status, "pending")} AND ${op.gt(attr.ttl, 0)}`)
```
## Conditional Writes
```typescript
// Conditional update — fail if condition not met
await Task.update({ taskId, projectId })
.set({ status: "done" })
.where((attr, op) => op.eq(attr.status, "active"))
.go();
```
## Execution Options
```typescript
// Inspect params without executing
const params = Task.get({ taskId, projectId }).params();
// Override table at runtime
await Task.get({ taskId, projectId }).go({ table: "other-table" });
// Include pk/sk in response (stripped by default)
await Task.get({ taskId, projectId }).go({ data: "includeKeys" });
// Consistent read
await Task.get({ taskId, projectId }).go({ consistent: true });
// Project specific attributes
await Task.get({ taskId, projectId }).go({ attributes: ["status", "createdAt"] });
```
## Single-Table Design Patterns
**One table, generic key names** — ElectroDB namespaces items by `model.entity` automatically. Items from different entities on the same table never collide.
**TTL** — declare as `{ type: "number" }` attribute, enable TTL on the field in DynamoDB console/CDK. Set epoch-seconds value at write time.
```typescript
const ttl = Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7; // 7 days
await Task.put({ ...attrs, ttl }).go();
```
**Service** — group multiple Entity objects for cross-entity queries:
```typescript
import { Service } from "electrodb";
const AppService = new Service({ Task, Employee });
// AppService.collections.<collectionName>(...).go()
```
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Querying a partial composite pk | All pk composites required; sk composites are optional prefix |
| Using `update` on non-existent item | Use `patch` to get `ConditionalCheckFailed` or `upsert` |
| Forgetting `.go()` | Expressions are lazy — nothing executes without `.go()` |
| Raw `DynamoDBClient` for DynamoDB ops | Always use entity methods; raw client bypasses type safety |
| Bumping `model.version` without migration | Changes key format — old items become invisible |
| `set` on a `readOnly` attribute | Throws at runtime; set defaults only via `default` |
## Additional resources
- `scripts/new-entity.sh <EntityName>` — scaffolds `src/entities/<entity-name>.entity.ts` with a ready-to-edit `new Entity({...})` skeleton (model namespace, `createdAt`/`updatedAt`/`ttl` attributes, a primary `pk`/`sk` composite index, and one GSI). Derives a kebab-case filename and a PascalCase export from the argument, and never overwrites an existing file. Use it to start a new entity instead of hand-copying the boilerplate above.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!