templates/forms/.agents/skills/storing-data/SKILL.md
How and where to store application data. Use when adding new data models, deciding between settings vs Drizzle tables, reading/writing app config, or working with application state.
npx skillsauth add BuilderIO/agent-native storing-dataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
All data lives in one SQL database. In production, set DATABASE_URL to point to Turso, Neon, Supabase, or D1 — same code, no changes needed.
There are three storage layers, each for a different kind of data:
Key-value store for persistent config that the user or agent can change. Theme, preferences, integration API keys, availability schedules.
import { getSetting, putSetting } from "@agent-native/core/settings";
// Read (returns null if not set)
const prefs = await getSetting("user-preferences");
// Write (creates or replaces)
await putSetting("user-preferences", { theme: "dark", density: "comfortable" });
From scripts:
import { readSetting, writeSetting } from "@agent-native/core/settings";
const prefs = await readSetting("user-preferences");
SSE: writes automatically notify the UI via { source: "settings", type: "change", key }.
For state the agent and UI share in real-time: what the user is looking at, compose drafts, navigation commands. Scoped by session — cleared between sessions.
import { readAppState, writeAppState, deleteAppState, listAppState } from "@agent-native/core/application-state";
// Write state (UI updates instantly via SSE)
await writeAppState("navigate", { view: "forms" });
// Read state
const nav = await readAppState("navigation");
// List by prefix
const drafts = await listAppState("compose-");
// Delete (one-shot commands: UI reads, then agent or UI deletes)
await deleteAppState("navigate");
SSE: writes automatically notify the UI via { source: "app-state", type: "change", key }.
For data with schemas, relationships, and queries: forms, responses. Define tables in server/db/schema.ts using Drizzle ORM.
import { table, text, integer } from "@agent-native/core/db/schema";
export const forms = table("forms", {
id: text("id").primaryKey(),
title: text("title").notNull(),
status: text("status").notNull(),
});
Query via getDb() singleton from server/db/index.ts.
For OAuth tokens acquired at runtime (Google, etc.). Never store these in settings — use the dedicated encrypted store.
import { saveOAuthTokens, getOAuthTokens, listOAuthAccounts } from "@agent-native/core/oauth-tokens";
await saveOAuthTokens("google", "[email protected]", { access_token: "...", refresh_token: "..." });
const tokens = await getOAuthTokens("google", "[email protected]");
const accounts = await listOAuthAccounts("google");
| Data | Layer | Why | |------|-------|-----| | User preferences, theme, config | Settings | Persistent KV, SSE notifications, simple read/write | | What the user sees on screen | Application State | Ephemeral, real-time sync, agent <-> UI bridge | | Navigation commands | Application State | Temporary, deleted when done | | Form definitions, responses | Drizzle table | Needs schema, queries, relationships | | OAuth refresh tokens | OAuth Tokens | Secure, per-provider, per-account |
Infrastructure config stays in .env — these differ per deployment:
DATABASE_URL — database connection (default: file:./data/app.db)DATABASE_AUTH_TOKEN — for remote databasesGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET — OAuth app credentialsACCESS_TOKEN — production auth tokenEverything else (user settings, tokens, app state) goes in SQL.
tools
Public booking flow — the state machine, animations, and URL/app-state sync.
tools
Trigger-based automations — reminders, follow-ups, webhooks — across the booking lifecycle.
tools
Team event types, round-robin assignment, collective bookings, host weights, and no-show calibration.
development
The pure `computeAvailableSlots` function — inputs, outputs, invariants, and debugging guide.