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
| Name | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Key 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
| Name | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Key to write (1–512 chars). |
| value | unknown | Yes | Any JSON-serializable value, up to 64KB serialized. |
| options.ttlSeconds | number | No | Optional 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
| Name | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Key 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
| Name | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | Counter key. |
| delta | number | No | Amount 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
getfollowed by aset. - 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
| Name | Type | Required | Description |
|---|---|---|---|
| keys | string[] | Yes | Keys 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
| Name | Type | Required | Description |
|---|---|---|---|
| options.prefix | string | No | Only return keys starting with this prefix. |
| options.limit | number | No | Page size, 1–1000 (defaults to 100). |
| options.cursor | string | No | Cursor 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.
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.
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.incrfor counters — agetfollowed by asetis not transactional and loses updates under concurrent invocations. - Adopt key-prefix conventions like
entry:<id>and scan one collection at a time withkv.list({ prefix }). kv.listreturns keys in ascending order — for newest-first listings, store an inverted-timestamp sort key (see the Guestbook template).- Set
ttlSecondson 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.