Security Best Practices for Schema Changes in Agent-Native: A Complete Guide

Agent-Native requires additive-only migrations, strict access control via the sharing layer, and safe defaults to protect production data during schema evolution.

All application data in Agent-Native lives in a Drizzle-managed SQL database. When modifying the data model, developers must follow strict security protocols to prevent data loss, unauthorized access, and destructive changes in production environments.

Apply Only Additive Migrations to Prevent Data Loss

Agent-Native mandates additive migrations only—you must never drop, rename, truncate, or destructively alter existing tables or columns. Removing or renaming columns can permanently erase user data and break existing actions that rely on the old schema.

Write migrations that only add new tables, columns, or indexes. Use Drizzle’s helper methods to extend table definitions safely:

import { table, text, now } from "@agent-native/core/db/schema";

export const posts = table("posts", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  // Safe additive change: new column with default value
  publishedAt: text("published_at")
    .default(now())          // Provides value for existing rows
    .notNull(),
});

The now() helper used above is defined in packages/core/src/db/schema.ts and provides a safe default timestamp for existing records during migration.

Avoid Destructive Commands in Production Environments

Never run drizzle-kit push on production databases. The push command unapplies safety checks and can apply destructive changes automatically without explicit migration files or audit trails.

Instead, deploy migrations via the standard drizzle migration:run command. This approach requires an explicit migration file that is audited by your CI pipeline, ensuring every change is reviewed and version-controlled before reaching production.

Enforce Row-Level Security with the Sharing Layer

Direct table access bypasses Agent-Native’s ownership model and can expose data across tenants. Every read and write operation must be scoped through the sharing/access layer.

Using accessFilter for Reads

For queries, use accessFilter to enforce row-level security based on the principal ID:

import { db } from "~/server/db";
import { posts } from "./schema";
import { accessFilter } from "@agent-native/core/sharing";

export async function getUserPosts(userId: string) {
  return db
    .select()
    .from(posts)
    .where(accessFilter(posts, postsShares, { principalId: userId }));
}

This pattern is documented in the "Data And Security" section of AGENTS.md, which emphasizes that all data access must respect the framework’s sharing semantics.

Using ownableColumns for Table Definitions

When defining tables that require ownership tracking, use the sharing schema helpers to automatically include security columns:

import {
  ownableColumns,
  createSharesTable,
} from "@agent-native/core/sharing/schema";

export const notes = table("notes", {
  id: text("id").primaryKey(),
  content: text("content").notNull(),
  ...ownableColumns,               // Adds owner_email, visibility, etc.
});

export const notesShares = createSharesTable(notes);

The ownableColumns spread operator and createSharesTable function are defined in packages/core/src/sharing/schema.ts and enforce consistent ownership and sharing semantics across the application.

Define Safe Defaults for New Columns

Adding a non-nullable column without a default value breaks existing rows during migration. Always define new columns with .default(<value>) to ensure backward compatibility.

For example, when adding a status field, specify a safe default that applies to all existing records:

text("status").notNull().default("new")

This practice prevents migration failures and maintains data integrity for existing rows.

Keep Secrets Out of Schema Definitions

Never embed secrets, credentials, or sensitive configuration in schema definitions. Hard-coded secrets become part of the codebase history and can be extracted from the repository even if removed later.

Instead, use Vault/Builder secrets or environment variables and reference them only at runtime. Keep schema files focused strictly on data structure, not configuration or secrets.

Validate Changes in Isolated Environments

Run every migration in an isolated staging environment before applying it to production. Detecting runtime errors early prevents data corruption in live systems.

Use the scripts/guard-* utilities provided in the repository to verify migrations against a staging database. This step ensures that additive changes work correctly and that access filters function as expected before production deployment.

Summary

  • Additive migrations only: Never drop or rename columns; only add new tables, columns, or indexes with safe defaults.
  • Avoid drizzle-kit push in production: Use drizzle migration:run for audited, explicit migrations.
  • Scope all access: Use accessFilter for queries and ownableColumns for table definitions to enforce row-level security.
  • No secrets in schema: Store credentials in environment variables or Vault, never in schema files.
  • Test before production: Validate migrations using staging environments and guard scripts.

Frequently Asked Questions

What happens if I run drizzle-kit push on production?

Running drizzle-kit push on production bypasses safety checks and can automatically apply destructive changes like column drops or table truncations without migration file review. This risks permanent data loss and breaks existing application logic. Always use drizzle migration:run instead.

How do I add a non-nullable column without breaking existing data?

Use the .default(<value>) method when defining the new column. For example, text("status").notNull().default("new") ensures existing rows receive the default value during migration. The now() helper from packages/core/src/db/schema.ts provides safe timestamp defaults.

What is the sharing/access layer in Agent-Native?

The sharing/access layer is a security framework that enforces row-level ownership and permissions. It includes accessFilter for scoped reads, resolveAccess and assertAccess for mutations, and schema helpers like ownableColumns defined in packages/core/src/sharing/schema.ts. This layer prevents unauthorized cross-tenant data access.

Where should I store sensitive credentials instead of the schema?

Store sensitive credentials in environment variables or a secure vault system (such as Builder secrets or HashiCorp Vault). Reference these values at runtime in your application code, never in packages/core/src/db/schema.ts or other schema definition files.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →