KV Storage
Every TMA.sh project includes a dedicated KV (key-value) namespace backed by Cloudflare KV. No setup or configuration required — it is provisioned automatically when you create a project.
Client-side API
Section titled “Client-side API”The SDK provides a simple interface for reading and writing KV data from your Mini App:
import { createTMA } from '@tma.sh/sdk';
const tma = createTMA({ projectId: 'your-project-id' });
// Store a valueawait tma.kv.set('user:preferences', { theme: 'dark', language: 'en',});
// Retrieve a valueconst prefs = await tma.kv.get('user:preferences');// { theme: 'dark', language: 'en' }
// Remove a valueawait tma.kv.remove('user:preferences');
// List keys by prefixconst result = await tma.kv.list('user:123:');// result.keys = [{ name: 'user:123:preferences' }, { name: 'user:123:scores' }]// result.list_complete = trueconst keyNames = result.keys.map(k => k.name);Values are automatically serialized to JSON. The client-side API routes requests through your project’s API endpoint, so authentication is enforced — users can only access KV data scoped to your project.
Server-side API
Section titled “Server-side API”In API routes, access the raw KV namespace binding directly:
import { Hono } from 'hono';
type Env = { Bindings: { KV: KVNamespace; };};
const app = new Hono<Env>();
app.get('/api/leaderboard', async (c) => { const leaderboard = await c.env.KV.get('leaderboard', 'json'); return c.json(leaderboard ?? []);});
app.post('/api/leaderboard/score', async (c) => { const { userId, score } = await c.req.json();
const leaderboard = await c.env.KV.get('leaderboard', 'json') ?? []; const updated = [...leaderboard, { userId, score }] .sort((a, b) => b.score - a.score) .slice(0, 100);
await c.env.KV.put('leaderboard', JSON.stringify(updated)); return c.json({ rank: updated.findIndex((e) => e.userId === userId) + 1 });});
export default app;You can also use the SDK’s server helper for a higher-level API:
import { createKV } from '@tma.sh/sdk/server';
app.get('/api/config', async (c) => { const kv = createKV(c.env.KV); const config = await kv.get('app-config'); return c.json(config);});KV with expiration
Section titled “KV with expiration”Set a TTL (time-to-live) on keys for automatic expiration. TTL is available server-side only — the client-side tma.kv.set() method signature is set(key: string, value: unknown) with no TTL option.
// Server-side (raw KV binding): expires in 1 hourawait c.env.KV.put('session:abc', JSON.stringify(data), { expirationTtl: 3600,});
// Server-side (createKV helper): also supports TTLconst kv = createKV(c.env.KV);await kv.set('cache:feed', feedData, { ttl: 3600 });Limits
Section titled “Limits”| Resource | Limit |
|---|---|
| Max value size | 128 KB (131,072 bytes) |
| Max key length | 512 bytes |
| Consistency | Eventually consistent (~60s) |
| Reads | Unlimited |
| Writes | 1,000 per second per key |
KV is eventually consistent, meaning a write in one region may take up to 60 seconds to propagate globally. For most Mini App use cases this is not noticeable.
Common use cases
Section titled “Common use cases”- User preferences — theme, language, notification settings
- Feature flags — toggle features without redeploying
- Leaderboards — store and sort scores for game-style apps
- Session data — temporary state that expires automatically
- Cached API responses — reduce external API calls with TTL-based caching
When to use something else
Section titled “When to use something else”KV storage is not a database. It does not support queries, indexes, transactions, or relational data. For those needs, connect to an external database from your API routes:
- Supabase — PostgreSQL with auth and real-time subscriptions
- Turso — SQLite at the edge with libSQL
- PlanetScale — serverless MySQL
See API Routes for examples of connecting to external databases.