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

> Learn Agent-Native security best practices for schema changes. Explore additive-only migrations, access control, and safe defaults to protect your production data.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: best-practices
- Published: 2026-06-28

---

**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:

```typescript
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`](https://github.com/BuilderIO/agent-native/blob/main/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:

```typescript
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`](https://github.com/BuilderIO/agent-native/blob/main/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:

```typescript
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`](https://github.com/BuilderIO/agent-native/blob/main/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:

```typescript
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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/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`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/db/schema.ts) or other schema definition files.