Skip to content

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.

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 value
await tma.kv.set('user:preferences', {
theme: 'dark',
language: 'en',
});
// Retrieve a value
const prefs = await tma.kv.get('user:preferences');
// { theme: 'dark', language: 'en' }
// Remove a value
await tma.kv.remove('user:preferences');
// List keys by prefix
const result = await tma.kv.list('user:123:');
// result.keys = [{ name: 'user:123:preferences' }, { name: 'user:123:scores' }]
// result.list_complete = true
const 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.

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);
});

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 hour
await c.env.KV.put('session:abc', JSON.stringify(data), {
expirationTtl: 3600,
});
// Server-side (createKV helper): also supports TTL
const kv = createKV(c.env.KV);
await kv.set('cache:feed', feedData, { ttl: 3600 });
ResourceLimit
Max value size128 KB (131,072 bytes)
Max key length512 bytes
ConsistencyEventually consistent (~60s)
ReadsUnlimited
Writes1,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.

  • 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

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.