Storage (KV)

Use @hostfunc/sdk/kv for built-in key-value storage scoped to each function. Values are JSON, persistence is automatic, and there is zero setup — every function gets its own store.

Step-by-Step Guide

1.Ready-made templates

The template picker's "storage" category ships working functions built on KV — fork one instead of starting blank.

  • Live poll — atomic vote counters with kv.incr.
  • Guestbook — prefix-scanned entries with inverted-timestamp sort keys for newest-first listing.
  • Link shortener, Page-view counter, Waitlist signup, and Feedback widget.

SDK API Reference

Import with import { kv } from "@hostfunc/sdk/kv";. Keys are strings (1–512 chars); values are any JSON-serializable data up to 64KB serialized. No configuration or integration setup is required.

kv.get

await kv.get<T = unknown>(key: string): Promise<T | null>

Read one value. JSON is parsed back into the stored shape.

Arguments

NameTypeRequiredDescription
keystringYesKey to read (1–512 chars).

Returns

The stored value, or null when the key is missing or expired.

kv.set

await kv.set(key: string, value: unknown, options?: { ttlSeconds?: number }): Promise<void>

Write a JSON-serializable value, creating or overwriting the key.

Arguments

NameTypeRequiredDescription
keystringYesKey to write (1–512 chars).
valueunknownYesAny JSON-serializable value, up to 64KB serialized.
options.ttlSecondsnumberNoOptional expiry in seconds; the key reads as missing after it elapses.

Returns

Resolves once the write is stored.

Notes

  • TTL is clamped to a maximum of 1 year.

kv.delete

await kv.delete(key: string): Promise<boolean>

Remove a key.

Arguments

NameTypeRequiredDescription
keystringYesKey to delete.

Returns

true when a key was deleted, false when it did not exist.

kv.incr

await kv.incr(key: string, delta = 1): Promise<number>

Atomically increment a numeric value. Missing keys are created at 0 first, then the delta is applied.

Arguments

NameTypeRequiredDescription
keystringYesCounter key.
deltanumberNoAmount to add (defaults to 1).

Returns

The new value after the increment.

Throws

  • kv_not_a_number (409) when the existing value is not numeric.

Notes

  • The increment is atomic — safe under concurrent invocations, unlike a get followed by a set.
  • Incrementing an expired key restarts it from 0 with no TTL.

kv.getMany

await kv.getMany<T = unknown>(keys: string[]): Promise<Record<string, T | null>>

Batch-read up to 100 keys in one call.

Arguments

NameTypeRequiredDescription
keysstring[]YesKeys to read (up to 100).

Returns

A record keyed by the requested keys; missing or expired keys map to null.

kv.list

await kv.list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{ keys: string[]; cursor: string | null }>

Page through keys in ascending key order, optionally filtered by prefix.

Arguments

NameTypeRequiredDescription
options.prefixstringNoOnly return keys starting with this prefix.
options.limitnumberNoPage size, 1–1000 (defaults to 100).
options.cursorstringNoCursor from a previous page.

Returns

{ keys, cursor } — pass cursor back to fetch the next page; null means no more pages.

SDK Code Examples

Live poll with atomic counters

Use kv.incr for votes and kv.getMany to read all tallies at once.

typescript
import { kv } from "@hostfunc/sdk/kv";const OPTIONS = ["tabs", "spaces"];export async function main(input: { action?: string; option?: string }) {  if (input.action === "vote" && OPTIONS.includes(input.option ?? "")) {    const votes = await kv.incr(`vote:${input.option}`);    return { ok: true, votes };  }  const counts = await kv.getMany<number>(OPTIONS.map((o) => `vote:${o}`));  return { results: OPTIONS.map((o) => ({ option: o, votes: counts[`vote:${o}`] ?? 0 })) };}

Prefix listing, newest first

Store entries under a shared prefix with an inverted-timestamp sort key so kv.list (ascending) returns newest entries first.

typescript
import { kv } from "@hostfunc/sdk/kv";export async function main(input: { message?: string }) {  if (input.message) {    const sortKey = String(9_999_999_999_999 - Date.now()).padStart(13, "0");    await kv.set(`entry:${sortKey}`, { message: input.message, at: new Date().toISOString() });    return { ok: true };  }  const { keys } = await kv.list({ prefix: "entry:", limit: 20 });  const entries = await kv.getMany<{ message: string; at: string }>(keys);  return { entries: keys.map((key) => entries[key]).filter((entry) => entry !== null) };}

Best Practices

  • Use kv.incr for counters — a get followed by a set is not transactional and loses updates under concurrent invocations.
  • Adopt key-prefix conventions like entry:<id> and scan one collection at a time with kv.list({ prefix }).
  • kv.list returns keys in ascending order — for newest-first listings, store an inverted-timestamp sort key (see the Guestbook template).
  • Set ttlSeconds on cache-style entries so stale data expires instead of counting against the per-function key limit.
  • Keep values under 64KB serialized — store IDs or references to large payloads, not the payloads themselves.

Related Documentation