Skip to content
cccskills
FreePublished on GitHub

storing-data

Plan where application data belongs and how agents and interfaces read and write it. Use SQL with Drizzle for structured records, choose dedicated blob storage for large files, and follow migration and access patterns that keep data durable and safe.

OriginalShow the original 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.

BuilderIO/agent-native7,519 stars(GitHub 7,519 + cccskills 0)
Works withPostgreSQL

Run score Not tested

See how to install

Good for

  • Choose a store for a new application data model
  • Add SQL tables and queries with Drizzle
  • Decide where uploaded files and binary data belong
  • Inspect or maintain application records

Files included (1)

  • SKILL.md13.4 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Storing Data — SQL is the Source of Truth

Rule

All application data lives in SQL (local PGlite, hosted Postgres in production). The agent and UI share the same database. SQL stores structured records, metadata, references, and searchable text — not large raw file payloads. Do not store durable app data in the filesystem unless the app is explicitly running a Local File Mode artifact flow described below.

Large binary or file-like payloads (images, video/audio, PDFs, ZIPs, screenshots, session replay chunks, thumbnails, generated assets, data: URLs, and base64 file bodies) must go through configured file/blob storage such as uploadFile() or putPrivateBlob(). Persist only the returned URL, asset id, or opaque blob handle in SQL. If storage is unavailable in hosted or persistent-database mode, fail closed with setup guidance instead of falling back to base64 in application_state, settings, resources, or app tables.

Local File Mode exception: some artifact apps (Content, Plans, Slides, Dashboards, Designs, etc.) can intentionally use repo files as the source of truth for the artifact itself. This must be explicit via agent-native.json, AGENT_NATIVE_MODE=local-files, or an app-owned local-file action helper. In that mode, the UI and agent still go through app actions, but those actions read/write scoped files through @agent-native/core/local-artifacts instead of SQL rows. App state, auth, settings, credentials, collaboration metadata, and hosted database mode remain SQL. File-to-database or file-to-provider synchronization is an explicit sync step, not an implicit side effect of editing.

When you add a data model, a list, or a read path, also follow the performance skill: project only the columns a list renders, index the columns hot queries filter/sort on, and avoid query waterfalls — so apps stay fast as data grows.

How It Works

Agent-Native apps use Drizzle ORM over PostgreSQL. Local development uses PGlite at data/pglite; production and shared preview deploys need a persistent hosted PostgreSQL DATABASE_URL.

For app code, use Drizzle's schema/query DSL by default. Raw SQL is an escape hatch for additive migrations, health checks, or one-off maintenance, not the normal way to build features.

Migration ownership

When the project contains drizzle.config.ts and drizzle/START_HERE.md, that managed Drizzle scaffold is the only app migration path. Define app tables in drizzle/schema.ts, run pnpm db:generate, and apply them with pnpm db:migrate. scripts/migrate-production.ts is framework-only: do not create a parallel runMigrations([...]) list in server/plugins/db.ts or import an app migration runner into the release script.

In projects without that managed scaffold, every entry added to a framework runMigrations([...]) list (@agent-native/core/db) needs a unique name: slug (for example, name: "analytics-alert-rules-table") alongside its version. Never renumber or reuse version numbers on existing entries.

Why: version numbers alone are not a safe identity. Two branches that each independently extend the same migration list can ship different DDL under the same version numbers — whichever branch deploys first "claims" those version numbers in the bookkeeping table, and the other branch's DDL is silently treated as already applied even though it never ran. A name: slug is tracked independently of version numbers, so it applies exactly once per database regardless of what any other branch already recorded.

Existing unnamed migrations don't need to be renamed retroactively (the two gating strategies coexist), but any new entry should always carry a name.

Every migration must also be backward compatible, not just additive. Beta and production now migrate independently against the same shared database, so one lane's migration can run before the other lane's matching code deploy. A new ADD COLUMN ... NOT NULL with no DEFAULT breaks on the first existing row, and breaks any already-deployed INSERT that doesn't know the column exists yet — make the column nullable, give it a DEFAULT, or use a self-filling type (SERIAL, GENERATED ... AS IDENTITY), and backfill separately if it needs a real value. guard:additive-migrations enforces this.

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

StorePurposeAccess
application_stateEphemeral UI state (compose windows, navigation)readAppState() / writeAppState()
settingsPersistent KV config (preferences, app settings)getSetting() / putSetting()
oauth_tokensOAuth credentials@agent-native/core/oauth-tokens
sessionsAuth sessions@agent-native/core/server

Domain Data (per-template)

In a managed Drizzle scaffold, define the PostgreSQL schema in drizzle/schema.ts. Otherwise, define schema with Drizzle's PostgreSQL exports in server/db/schema.ts. Get a database instance with const db = getDb() from server/db/index.ts. All queries are async.

import { eq, sql } from "drizzle-orm";
import { boolean, pgTable, text } from "drizzle-orm/pg-core";

export const tasks = pgTable("tasks", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  completed: boolean("completed").notNull().default(false),
  createdAt: text("created_at").notNull().default(sql`now()`),
});

const rows = await db.select().from(tasks).where(eq(tasks.id, taskId));

Outside a managed Drizzle scaffold, use drizzle-orm/pg-core so app schemas state their PostgreSQL types directly.

TemplateTables
Mailemails, labels (+ Gmail API when connected)
Calendarevents, bookings
Formsforms, responses
Contentdocuments
Slidesdecks (JSON stored in SQL)
Videoscompositions in registry + localStorage

Agent Access

The agent uses app-specific actions to read/write the database. Core DB scripts are for inspection and maintenance, not for implementing normal product behavior:

  • 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 "UPDATE ..." — Last-resort ad-hoc maintenance for short columns, multi-column writes, or computed updates when no domain action exists. For several related writes, prefer --statements '[{"sql":"...","args":[...]}]' so they run sequentially in one transaction. Schema changes are blocked; use reviewed additive migrations/startup code instead.
  • 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 — always prefer these over raw SQL when one exists. They encode business rules, power the client action hooks, and for editor-backed tables (documents, slides) also push live Yjs updates to open collaborative editors. db-patch is the generic fallback for tables without a dedicated edit action.

For one-off maintenance, how to choose between db-exec UPDATE and db-patch:

ScenarioUse
SET status = 'published' on one rowdb-exec
SET calories = calories + 50db-exec
Updating several columns at oncedb-exec
Inserting/updating several rows as one logical operationdb-exec --statements
Fixing a typo in a 50KB markdown document's content columndb-patch
Changing a single key in a dashboard's JSON blobdb-patch
Tweaking one paragraph of slide HTML stored in decks.datadb-patch
Any edit where you'd otherwise re-send thousands of charactersdb-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 using React Query hooks from the client API. The framework owns the HTTP transport behind these hooks, so components should not call action routes with raw fetch.

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

// Read data
const { data } = useActionQuery("list-meals", { date: "2025-01-01" });

// Write data
const { mutate } = useActionMutation("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.

Production / Cloud Deployment

Local PGlite works out of the box for development. To deploy to production or any environment where data must survive restarts:

  1. Set DATABASE_URL to a persistent hosted PostgreSQL database.
  2. Keep schema and queries PostgreSQL-compatible.

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 Drizzle query builder methods (select, insert, update, delete) and standard operators from drizzle-orm (eq, and, or, inArray, desc, etc.) for app reads/writes
  • Use framework schema helpers from @agent-native/core/db/schema instead of direct Drizzle schema-driver imports
  • 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 uploadFile() or putPrivateBlob() for large files/blob data and store only URLs, ids, or handles in SQL
  • Use core DB scripts (db-schema, db-query, db-exec, db-patch) for ad-hoc database operations
  • Use db-exec --statements instead of several separate db-exec calls for related writes; it is faster and rolls back the whole batch if one statement fails
  • 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 store large files, base64 blobs, data: URLs, screenshots, videos, audio, PDFs, ZIPs, or session replay chunks directly in SQL rows, application_state, settings, or resources
  • Don't implement product features with raw SQL or getDbExec() when Drizzle can express the query
  • Write advanced SQL for PostgreSQL
  • 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
  • client-methods — Keep route details behind named client helpers/hooks
  • self-modifying-code — The agent can also modify the app's source code

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

a2a-assets

FreeSummary

Generate, refine, export, and place brand images or videos from an Assets library into another app. The skill covers choosing a library, handling image batches and video runs, and updating selected assets after feedback.

  • Generate brand images for slides or another app
  • Refine an existing asset after feedback
  • Create and export a video asset
BuilderIO/agent-native7.5KUpdated Oct 26, 2026

account-health

FreeSummary

Prepare a customer account health, renewal, or contract usage readout. Verify the account identity, use approved metric definitions, compare contracted capacity with actual usage, and trace findings to sources, time windows, and known gaps.

  • Preparing a named customer's account health review
  • Comparing contracted seats or capacity with usage
  • Gathering adoption signals before a renewal or QBR
BuilderIO/agent-native7.5KUpdated Oct 26, 2026

actions

FreeSummary

Create focused app actions that agents can call as tools and frontend code can use through client hooks. Use them to add provider integrations or fetch UI data, with validated inputs and structured results.

  • Adding a new app operation
  • Connecting an external provider API
  • Fetching data for a frontend view
BuilderIO/agent-native7.5KUpdated Oct 26, 2026

adding-a-feature

FreeSummary

Use this checklist when adding a feature or integration to an agent-native app. It covers the user interface, agent-callable actions, instructions, and app-state sync so both the user and agent can work with the capability consistently.

  • Planning a new feature across UI and agent actions
  • Checking that agent changes appear in the UI
  • Documenting actions and feature state for the agent
BuilderIO/agent-native7.5KUpdated Oct 26, 2026

adhoc-analysis

FreeSummary

Investigate complex questions by combining data from multiple sources, then summarize patterns, metrics, and evidence in a clear report. Save requested analyses as dashboards with instructions for future refreshes.

  • Comparing CRM deals with call records
  • Investigating customer health across systems
  • Saving a repeatable performance analysis
BuilderIO/agent-native7.5KUpdated Oct 26, 2026

admin-surfaces

FreeSummary

Use the Analytics admin surfaces to inspect and change workspace feature flags, review dashboard ownership and usage activity, or browse and edit databases connected from other agent-native apps. The skill guides administrators to the relevant view and actions.

  • Review dashboard owners, views, and edits
  • Change a workspace feature flag for an app
  • Browse or edit a connected app database
BuilderIO/agent-native7.5KUpdated Oct 26, 2026

See all skills by BuilderIO

Report a problem with this skill