KV Storage
KV (key-value) storage is the default storage type for Vulcan apps. It's fast, simple, and handles the vast majority of use cases with no hassle.
What it is
Think of KV like a giant dictionary. Your app stores named items (keys) with associated data (values). It's great for:
- Lists of records (tasks, contacts, tickets, inventory items)
- App settings and configuration
- Per-user data
- Anything where you're reading and writing individual items
When to use KV
Use KV when:
- You're building an app where users add, view, edit, and delete records
- You don't need complex multi-field queries or joins
- You want fast setup with no schema design
Use SQL instead when:
- You need to filter, sort, or aggregate across many fields at once
- You're building reports or analytics features
- The data is highly relational (lots of linked tables)
How to add it to your app
Just tell the AI:
"Store the form submissions in KV" "I need this data to persist between page loads" "Add KV storage to save and retrieve records"
The AI will provision a KV namespace and wire it up for you. You'll see it referenced as KV in your app's configuration.
Seed data and migrations
When the AI sets up KV storage for a new app, it creates a small migrations system inside src/migrations/. Migrations seed your app with example data so it doesn't look empty on first load — and they run exactly once, so they never overwrite data you've already added.
File structure
src/migrations/
runner.ts ← Migration engine (don't touch this)
index.ts ← Migration registry — import and register all migrations here
001-seed-data.ts ← First migration: seeds initial example data
002-add-items.ts ← Second migration (added later if needed)
...How it works
- On the first request to your app,
runMigrations()is called automatically fromdata.ts - It reads a
migrations:appliedkey in your KV namespace to see which migrations have already run - Any migration not in that list is run in order
- After each migration succeeds, its ID is added to
migrations:appliedso it never runs again
This means migrations are lazy — they run on demand, not at deploy time.
Adding or changing seed data
Never edit an existing migration file. Once a migration has run, its ID is recorded in KV and the file is never executed again. Changing the file has no effect.
To add or update data, ask the AI:
"Add 5 more sample contacts to the seed data" "Seed the app with some example inventory items"
The AI will create a new numbered migration file (e.g. 002-add-contacts.ts) and register it at the end of src/migrations/index.ts. On the next request, only that new migration runs — existing data is untouched.
Manual reset (dev / testing)
If you want to wipe and re-seed your data during development, delete the migrations:applied key from your KV namespace. All migrations will run again on the next request.
You can ask the AI to do this:
"Reset the seed data so I can start fresh"
Limits
- Individual values have a maximum size of 25 MB
- KV is optimized for reads; heavy write workloads may be better suited to SQL
- Not ideal for data that needs complex relational queries
When KV starts to break down
KV works well for most internal tools, but there are patterns that signal it's time to switch to SQL:
- Large growing lists — if your app stores records as a single JSON array in one key (common in AI-generated apps), that array grows with every record. Once it reaches thousands of entries, reads and writes become slow and you risk hitting the 25 MB value limit. Switch to SQL, or ask the AI to paginate by splitting records across multiple keys.
- Filtering and sorting — KV has no query layer. If your app needs "show all orders from last week" or "sort contacts by last name", you're loading everything into memory and filtering in code. SQL handles this far more efficiently.
- Concurrent writes — KV uses last-writer-wins. If two users submit a form at the same moment, one write can silently overwrite the other. For high-traffic write paths, use SQL instead.
- Running reports or aggregates — counts, sums, averages across many records should live in SQL.
If you're not sure which to use, just ask the AI — it will pick the right storage type based on your description.