Store
A DynamoDB table with on-demand billing only — never idle, never provisioned.
Store is a thin, typed wrapper over DynamoDB's own access patterns. It's deliberately not an ORM — every method
maps to one concrete DynamoDB operation, so there's never a hidden N+1 query or an unpredictable scan hiding behind a
friendly-looking call.
Declaring a store
import { Store } from "@vorynza/wisp";
export const orders = new Store("orders", {
partitionKey: { name: "id", type: "S" },
});| Option | Type | Description |
|---|---|---|
partitionKey | { name: string; type: "S" | "N" | "B" } | Required. |
sortKey | { name: string; type: "S" | "N" | "B" } | Optional. |
indexes | GsiConfig[] | Optional global secondary indexes — each with its own partitionKey and optional sortKey. |
ttl | string | Optional. The item attribute DynamoDB should use for automatic expiry. |
Billing is always on-demand (PAY_PER_REQUEST). It isn't a configurable option — provisioned throughput would break
the $0-idle guarantee wisp enforces on every deploy.
Methods
interface StoreClient {
get(key: StoreKey): Promise<StoreItem | undefined>;
put(item: StoreItem): Promise<StoreItem>;
update(key: StoreKey, patch: StoreItem): Promise<StoreItem>;
delete(key: StoreKey): Promise<void>;
query(input: QueryInput): Promise<StoreItem[]>;
batchGet(keys: StoreKey[]): Promise<StoreItem[]>;
transactWrite(operations: TransactWriteOperation[]): Promise<void>;
scanFullTableIKnowWhatImDoing(): Promise<StoreItem[]>;
}StoreKey and StoreItem are both plain Record<string, unknown> — wisp doesn't impose a schema beyond your
declared partition/sort keys.
query
const pending = await orders.query({
partitionKey: "pending",
index: "byStatus",
sortKeyCondition: { op: "beginsWith", value: "2026-" },
limit: 20,
});sortKeyCondition.op accepts "eq" | "lt" | "lte" | "gt" | "gte" | "beginsWith". Omit index to query the table's
own partition key instead of a GSI.
scanFullTableIKnowWhatImDoing
The name is the warning. A full table scan is the one access pattern wisp won't make convenient to reach for by accident — it's still one call away when you genuinely need it (a one-off migration, an admin tool), but nothing else in this API nudges you toward it.
Global secondary indexes
export const orders = new Store("orders", {
partitionKey: { name: "id", type: "S" },
indexes: [{ name: "byStatus", partitionKey: { name: "status", type: "S" } }],
});A function that queries a GSI'd store gets read access to every index the table has, not just the one queried —
determining which specific index a given .query() call targets would need deeper call-argument analysis than the
compiler performs today. It's a documented, low-severity gap: a read grant, not a write grant.
What this deploys to
One AWS::DynamoDB::Table, BillingMode: PAY_PER_REQUEST, with one AWS::DynamoDB::GlobalSecondaryIndex per
declared index. Any function that reads from or writes to the store gets exactly the DynamoDB actions its code calls
— dynamodb:Query/GetItem/BatchGetItem for reads, dynamodb:PutItem/UpdateItem for writes,
dynamodb:DeleteItem for deletes — scoped to this table's ARN (and its GSI ARNs, for reads). Never a wildcard
resource.

