wispwisp
Primitives

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" },
});
OptionTypeDescription
partitionKey{ name: string; type: "S" | "N" | "B" }Required.
sortKey{ name: string; type: "S" | "N" | "B" }Optional.
indexesGsiConfig[]Optional global secondary indexes — each with its own partitionKey and optional sortKey.
ttlstringOptional. 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.

On this page