Query a Collection
Most views show part of a collection, such as the pending orders, one
customer's orders, or this month's log entries. puter.kv looks records
up by key prefix, so the fields you filter on go in the key. This builds on
Store a Large Collection, where each record
has its own key.
Put the Filter in the Key
To query by a field, put it in the key before the record's ID when you write the
record with the puter.kv.set() method:
// order:<status>:<id>
await puter.kv.set(`order:${ status }:${ id }`, order);
To read one slice, use the puter.kv.list() method with that
prefix. Passing true as the second argument returns the values with the keys:
const pending = await puter.kv.list('order:pending:', true);
// [{ key: 'order:pending:0001', value: { customer: 'alice', total: 48 } }, ...]
This short form returns every match in one array. For a slice that can grow
past a few hundred records, load it a page at a time with a limit and a
cursor, as in Store a Large Collection.
How a Pattern Matches
The puter.kv.list() method matches a prefix of the key, byte for
byte:
await puter.kv.list('order:pending:'); // every pending order
await puter.kv.list('order:pending:*'); // the same, a trailing * is optional
await puter.kv.list('order:pend'); // also matches, a prefix can stop mid-segment
await puter.kv.list('order:*:alice:'); // a * before the end is a literal asterisk
await puter.kv.list('ORDER:pending:'); // empty, matching is case-sensitive
Lowercase the segments you build keys from, and the case a value arrives in stops mattering:
await puter.kv.set(`order:${ status.toLowerCase() }:${ id }`, order);
Filter on a Second Field
Every prefix of a key is a filter you can read, so an extra segment adds a filter:
// order:<status>:<customer>:<id>
await puter.kv.set(`order:${ status }:${ customer }:${ id }`, order);
await puter.kv.list('order:pending:', true); // by status
await puter.kv.list('order:pending:alice:', true); // by status and customer
The segments read left to right, so this layout answers "pending" and "pending for alice", while "everything for alice" needs its own key. Put the field every view filters on first, and the one only some views add second.
Query by Another Field
To filter by a field that isn't at the start of the key, write a second key that
starts with that field. Pass both to puter.kv.set() as an array of
{ key, value } items to write them in one request:
await puter.kv.set([
{ key: `order:${ status }:${ id }`, value: order },
{ key: `order-by-customer:${ customer }:${ id }`, value: order },
]);
Reading by customer is then one query, the same as any other prefix:
const rows = await puter.kv.list('order-by-customer:alice:', true);
Both keys hold the order, so every change has to reach both, and deleting the order deletes both. Keep an Index in Sync shows how.
Filter the Rest in Your Code
A field you rarely filter on can stay out of the key. Narrow the read with a prefix first, then filter what came back:
const rows = await puter.kv.list('order:pending:', true);
const large = rows.filter(row => row.value.total > 50);
This works once the prefix has cut the set down to about a page, and it keeps rarely used filters out of the key layout.
Filter by Date
ISO 8601 timestamps sort in time order as text, so a prefix of one is a calendar range:
await puter.kv.set(`log:${ new Date().toISOString() }`, entry);
// log:2026-09-09T14:03:11.204Z
await puter.kv.list('log:2026-09-', true); // September
await puter.kv.list('log:2026-09-09', true); // one day
await puter.kv.list('log:2026-09-09T14', true); // one hour
A listing comes back in ascending key order. For newest first, pass
reverse: true in the options form:
await puter.kv.list({ pattern: 'log:2026-09-', returnValues: true, reverse: true });
For a range that isn't one prefix, such as the last 7 days, read newest first
and stop at the first key before the range. With stream: true, pages load one
at a time, so stopping early skips the rest:
async function logSince (date) {
const start = `log:${ date.toISOString() }`;
const entries = [];
for await ( const page of puter.kv.list({ pattern: 'log:', returnValues: true, reverse: true, limit: 100, stream: true }) ) {
for ( const entry of page.items ) {
if ( entry.key < start ) return entries;
entries.push(entry);
}
}
return entries;
}
const lastWeek = await logSince(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
Filter by Number
A number in a key sorts as text, so 10 lands before 9. Pad it to a fixed
width and the order comes out numeric, which also makes a digit prefix a range:
await puter.kv.set(`invoice:${ String(number).padStart(6, '0') }`, invoice);
// invoice:000042
await puter.kv.list('invoice:', true); // every invoice, in numeric order
await puter.kv.list('invoice:000', true); // 000000 to 000999
Pick a width the numbers won't outgrow. Once the count passes six digits,
invoice:1000000 sorts before invoice:999999 and the order breaks.