storing-data

$npx mdskill add BuilderIO/agent-native/storing-data

Centralize all application data in a single SQL database.

  • Eliminates filesystem dependencies for persistent application data.
  • Integrates SQLite, Drizzle ORM, and cloud database providers.
  • Automatically creates core stores for state, settings, and auth.
  • Delivers identical local and production behavior through unified storage.
SKILL.md
.github/skills/storing-dataView on GitHub ↗
---
name: storing-data
description: >-
  How to store application data in agent-native apps. All data lives in SQL.
  Use when adding data models, deciding where to store data, or reading/writing
  application data.
---

# Storing Data — SQL is the Source of Truth

## Rule

All application data lives in **SQL** (SQLite locally, cloud database in production). The agent and UI share the same database. There is no filesystem dependency for data.

## How It Works

Agent-native apps use SQLite via Drizzle ORM + `@libsql/client`. This works locally out of the box and upgrades seamlessly to cloud databases (Turso, Neon, Supabase, D1) by setting `DATABASE_URL`. **Local and production behave identically.**

### Core SQL Stores (auto-created, available in all templates)

| Store               | Purpose                                              | Access                                     |
| ------------------- | ---------------------------------------------------- | ------------------------------------------ |
| `application_state` | Ephemeral UI state (compose windows, navigation)     | `readAppState()` / `writeAppState()`       |
| `settings`          | Persistent KV config (preferences, app settings)     | `getSetting()` / `setSetting()`            |
| `oauth_tokens`      | OAuth credentials                                    | `@agent-native/core/oauth-tokens`          |
| `sessions`          | Auth sessions                                        | `@agent-native/core/server`               |

### Domain Data (per-template)

Define schema with Drizzle ORM in `server/db/schema.ts`. Get a database instance with `const db = getDb()` from `server/db/index.ts`. All queries are async.

| Template     | Tables                                        |
| ------------ | --------------------------------------------- |
| **Mail**     | emails, labels (+ Gmail API when connected)   |
| **Calendar** | events, bookings                              |
| **Forms**    | forms, responses                              |
| **Content**  | documents                                     |
| **Slides**   | decks (JSON stored in SQL)                    |
| **Videos**   | compositions in registry + localStorage       |

### Agent Access

The agent uses actions to read/write the database:

- `pnpm action db-schema` — Show all tables, columns, types
- `pnpm action db-query --sql "SELECT * FROM forms"` — Run SELECT queries
- `pnpm action db-exec --sql "INSERT INTO ..."` — Run INSERT / UPDATE / DELETE. Use for short columns, multi-column writes, computed updates.
- `pnpm action db-patch --table <t> --column <c> --where "<clause>" --find "<old>" --replace "<new>"` — **Surgical search/replace on a large text column.** Sends the diff instead of re-transmitting the whole value, so it's dramatically more token-efficient than `db-exec UPDATE` when editing multi-kilobyte documents, slide HTML, dashboard/form JSON, etc. Targets exactly one row per call — narrow `--where` by primary key. Supports `--edits '[{find,replace},...]'` for batch edits and `--all` to replace every occurrence.
- App-specific actions for domain operations (auto-exposed as HTTP endpoints) — **always prefer these over raw SQL when one exists.** They encode business rules, and for editor-backed tables (documents, slides) they also push live Yjs updates to open collaborative editors. `db-patch` is the generic fallback for tables without a dedicated edit action.

**How to choose between `db-exec UPDATE` and `db-patch`:**

| Scenario                                                       | Use          |
| -------------------------------------------------------------- | ------------ |
| `SET status = 'published'` on one row                          | `db-exec`    |
| `SET calories = calories + 50`                                 | `db-exec`    |
| Updating several columns at once                               | `db-exec`    |
| Fixing a typo in a 50KB markdown document's `content` column   | `db-patch`   |
| Changing a single key in a dashboard's JSON blob               | `db-patch`   |
| Tweaking one paragraph of slide HTML stored in `decks.data`    | `db-patch`   |
| Any edit where you'd otherwise re-send thousands of characters | `db-patch`   |

All of these honor the per-user / per-org data scoping — you can't read or write rows outside the current user's data, regardless of which tool you choose.

### Frontend Access

The frontend calls actions via their auto-mounted HTTP endpoints using React Query hooks:

```ts
import { useActionQuery, useActionMutation } from "@agent-native/core/client";

// Read data (calls GET /_agent-native/actions/list-meals)
const { data } = useActionQuery<Meal[]>("list-meals", { date: "2025-01-01" });

// Write data (calls POST /_agent-native/actions/log-meal)
const { mutate } = useActionMutation<Meal>("log-meal");
```

Actions are the **preferred way** for the frontend to access data. You rarely need custom `/api/` routes — only for file uploads, streaming, webhooks, or OAuth callbacks.

### Cloud Deployment

Local SQLite works out of the box. To deploy to production with a cloud database:

1. Set `DATABASE_URL` (e.g. `libsql://your-db.turso.io`)
2. Set `DATABASE_AUTH_TOKEN` for auth
3. No code changes needed — `@libsql/client` handles both local and remote

### Real-time Sync

Polling streams database changes to the UI. When the agent writes to the database via scripts, the UI updates automatically via `useDbSync()` which invalidates React Query caches.

## Do

- Use Drizzle ORM for structured domain data (forms, bookings, documents)
- Use the `settings` store for app configuration and user preferences
- Use `application-state` for ephemeral UI state that the agent and UI share
- Use `oauth-tokens` for OAuth credentials
- Use core DB scripts (`db-schema`, `db-query`, `db-exec`, `db-patch`) for ad-hoc database operations
- Reach for `db-patch` instead of `db-exec UPDATE` whenever you're making a small change to a large text/JSON column — it's much cheaper on tokens

## Don't

- Don't store structured app data as JSON files
- Don't store app state in localStorage, sessionStorage, or cookies (except for UI-only preferences like sidebar width)
- Don't keep state only in memory (server variables, global stores)
- Don't use Redis or any external state store for app data
- Don't interpolate user input directly into SQL queries — use Drizzle ORM's query builder

## Security

- **SQL injection** — Use Drizzle ORM's query builder, never raw string interpolation for SQL queries
- **Validate before writing** — Check data shape before writing, especially for user-submitted data

## Application State and Context Awareness

When storing app-state, include **navigation state** — the agent needs to know what the user is looking at. The `application_state` table holds ephemeral UI state that both the agent and UI share. Key patterns:

- **`navigation` key** — the UI writes current view and selection on every route change. The agent reads this before acting.
- **`navigate` key** — the agent writes one-shot commands to navigate the UI. The UI processes and deletes them.
- **Domain-specific keys** (e.g., `compose-{id}`) — bidirectional state for features like email drafts.

When adding a new data model or feature, also consider what navigation and selection state needs to be exposed via application-state. See the **context-awareness** skill for the full pattern.

## Related Skills

- **context-awareness** — How to expose navigation and selection state via application-state
- **real-time-sync** — Set up polling so the UI updates when the database changes
- **actions** — Create actions with `defineAction` to query the database (auto-exposed as HTTP endpoints)
- **self-modifying-code** — The agent can also modify the app's source code
More from BuilderIO/agent-native