Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/schema-diff-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@permify-toolkit/core": minor
"@permify-toolkit/cli": minor
---

feat: add schema diff command to preview changes before pushing

- New CLI command `schema diff` compares local schema against the deployed schema on the Permify server
- Supports local-vs-local comparison via `--source` flag (no server needed)
- Structural summary shows added, removed, and modified entities, relations, and permissions
- `--verbose` flag shows a unified text diff for full detail
- `--exit-code` flag exits with code 1 when changes are detected (for CI pipelines)
- New core exports: `readSchemaFromPermify`, `diffSchema`, `textDiff`
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ We're actively working on expanding the toolkit. Here's what's coming:
- [x] Relationship query CLI commands — list, inspect, and export existing relationships from a tenant
- [ ] Permission result caching — in-memory (and optionally Redis-backed) cache to reduce gRPC round-trips
- [ ] OpenTelemetry tracing — structured spans and metrics for permission checks and schema operations
- [ ] Schema diff CLI command — preview what will change before pushing a schema update
- [x] Schema diff CLI command — preview what will change before pushing a schema update
- [ ] Multi-tenant CLI management — create, list, and delete tenants directly from the CLI

Have ideas? [Open an issue](https://github.com/thisisnkc/permify-toolkit/issues) or start a [discussion](https://github.com/thisisnkc/permify-toolkit/discussions)!
Expand Down
111 changes: 106 additions & 5 deletions docs-site/docs/packages/cli.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
sidebar_position: 3
toc_max_heading_level: 4
---

# @permify-toolkit/cli
Expand Down Expand Up @@ -88,7 +89,9 @@ The `--tenant` flag is **optional** if `tenant` is defined in `permify.config.ts

## Commands

### `schema push`
### Schema

#### `schema push`

Pushes the schema defined in your config to the Permify server.

Expand Down Expand Up @@ -124,7 +127,7 @@ The Permify server validates your schema on push. If there are errors, you'll se
Error: Entity "usr" referenced in relation "document.owner" does not exist
```

### `schema validate`
#### `schema validate`

Validates your schema locally without connecting to a Permify server. Catches structural errors, broken references, permission cycles, and suspicious patterns before you push.

Expand Down Expand Up @@ -184,7 +187,105 @@ permify-toolkit schema validate && permify-toolkit schema push
Run `schema validate` before `schema push` for instant local feedback, no server connection needed.
:::

### `relationships seed`
#### `schema diff`

Previews what will change before pushing a schema update. Compares your local schema (from `permify.config.ts`) against the schema currently deployed on the Permify server — or against another local `.perm` file.

```bash
permify-toolkit schema diff [--tenant <id>] [flags]
```

**Flags:**

| Flag | Alias | Description | Required | Default |
| ----------------- | ----- | ------------------------------------------------------------------ | -------- | ----------- |
| `--tenant` | | Tenant ID to diff against | No | From config |
| `--create-tenant` | `-c` | Create tenant if it doesn't exist | No | `false` |
| `--source` | `-s` | Path to a `.perm` file to compare against (local-vs-local mode) | No | |
| `--verbose` | `-v` | Show raw unified text diff after the structural summary | No | `false` |
| `--exit-code` | `-e` | Exit with code 1 if changes are detected (useful for CI pipelines) | No | `false` |

**Examples:**

```bash
# Compare local schema against what's deployed on the server
permify-toolkit schema diff

# Compare against a specific tenant
permify-toolkit schema diff --tenant staging

# Compare two local schemas (no server connection needed)
permify-toolkit schema diff --source ./old-schema.perm

# Include a unified text diff for full detail
permify-toolkit schema diff --verbose

# CI mode — fail the pipeline if schema has drifted
permify-toolkit schema diff --exit-code
```

**Output:**

The command shows a structural summary of changes at the entity, relation, and permission level:

![Schema Diff Output](/img/schema-diff-output.png)

- **Green (`+`)** — added entities, relations, or permissions
- **Red (`-`)** — removed entities, relations, or permissions
- **Yellow (`~`)** — modified entities (contents changed), or individual relations/permissions whose definition changed

When no changes are detected:

```
✔ Schema is up to date — no changes detected (tenant: t1)
```

When no schema exists on the remote server yet (first-time push), everything is shown as additions:

```
Schema Diff — tenant: t1
ℹ No schema found on remote — showing full schema as additions
```

With `--verbose`, a unified text diff is appended below the structural summary:

```diff
--- remote (tenant: t1)
+++ local (permify.config.ts)
@@ -1,5 +1,8 @@
entity user {}
entity document {
relation owner @user
+ relation editor @user
- permission view = owner
+ permission view = owner or editor
+ permission edit = editor
}
```

**Exit codes:**

| Code | Meaning |
| ---- | ------------------------------------------------------- |
| `0` | No changes detected, or changes detected (default mode) |
| `1` | Changes detected and `--exit-code` flag is set |

:::tip Use in CI pipelines
Combine `--exit-code` with your CI to detect schema drift:

```bash
permify-toolkit schema diff --exit-code || echo "Schema has changed — review before pushing"
```

:::

:::tip Preview before push
Run `schema diff` before `schema push` to review exactly what will change on the server. Pair with `--verbose` for a complete picture.
:::

### Relationships

#### `relationships seed`

Seeds relationship data from a JSON file.

Expand Down Expand Up @@ -234,7 +335,7 @@ permify-toolkit relationships seed --tenant my-tenant-id --file-path ./data/rela
permify-toolkit relationships seed --tenant new-tenant-id --file-path ./relationships.json --create-tenant
```

### `relationships list`
#### `relationships list`

Queries and displays relationship tuples from a Permify tenant. Useful for debugging authorization — quickly see what relationships exist for a given entity type.

Expand Down Expand Up @@ -320,7 +421,7 @@ When no relationships match:
Use `relationships list` to verify that the tuples you expect actually exist in Permify. If a permission check fails unexpectedly, list the relationships for that entity to see what's stored.
:::

### `relationships export`
#### `relationships export`

Exports relationship tuples from a Permify tenant to a JSON file. The output format is identical to the `relationships seed` input format — so you can export from one tenant and seed into another.

Expand Down
152 changes: 134 additions & 18 deletions docs-site/docs/packages/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,119 @@ await deleteRelationships(client, {
});
```

## Reading Schemas

Read the currently deployed schema from a Permify server. This is useful for inspecting what's live, building migration scripts, or comparing against a local definition.

```typescript
import {
createPermifyClient,
readSchemaFromPermify
} from "@permify-toolkit/core";

const client = createPermifyClient({
endpoint: "localhost:3478",
insecure: true
});

const result = await readSchemaFromPermify({
client,
tenantId: "my-tenant"
});

if (result.schema) {
console.log("Deployed schema DSL:\n", result.schema);
console.log("Entities:", Object.keys(result.entities));
// e.g. { user: { relations: {}, permissions: {} }, document: { relations: { owner: "@user" }, permissions: { edit: "owner" } } }
} else {
console.log("No schema deployed yet for this tenant.");
}
```

The returned `entities` map contains each entity's relations and permissions with their definitions — relation targets (e.g., `@user`) and permission expressions (e.g., `owner or editor`).

## Schema Diffing

Compare two schemas structurally to see what entities, relations, and permissions were added, removed, or changed. This powers the CLI's `schema diff` command, but you can use it directly for custom CI checks, migration previews, or programmatic workflows.

### Structural Diff

`diffSchema` compares two entity maps and returns a structured result:

```typescript
import {
readSchemaFromPermify,
diffSchema,
type SchemaEntityMap
} from "@permify-toolkit/core";

// Define your local schema as a flat entity map
const local: SchemaEntityMap = {
user: { relations: {}, permissions: {} },
document: {
relations: { owner: "@user", editor: "@user" },
permissions: { view: "owner or editor", edit: "owner" }
}
};

// Read the deployed schema from the server
const remote = await readSchemaFromPermify({ client, tenantId: "my-tenant" });

const result = diffSchema(local, remote.entities);

if (!result.hasChanges) {
console.log("Schemas are identical.");
} else {
for (const entity of result.added) {
console.log(`+ New entity: ${entity.name}`);
}
for (const entity of result.removed) {
console.log(`- Removed entity: ${entity.name}`);
}
for (const entity of result.modified) {
console.log(`~ Modified: ${entity.name}`);
console.log(" Relations added:", entity.relations.added);
console.log(" Relations removed:", entity.relations.removed);
console.log(" Relations changed:", entity.relations.changed);
console.log(" Permissions added:", entity.permissions.added);
console.log(" Permissions removed:", entity.permissions.removed);
console.log(" Permissions changed:", entity.permissions.changed);
}
}
```

This is useful for building custom guardrails — for example, failing a deploy if permissions were removed without an explicit approval step.

### Text Diff

`textDiff` generates a unified diff (like `git diff`) between two schema DSL strings:

```typescript
import { textDiff } from "@permify-toolkit/core";

const remoteDsl =
"entity user {}\nentity document {\n relation owner @user\n permission view = owner\n}";
const localDsl =
"entity user {}\nentity document {\n relation owner @user\n relation editor @user\n permission view = owner or editor\n}";

const diff = textDiff(localDsl, remoteDsl, "local", "remote");

if (diff) {
console.log(diff);
// --- remote
// +++ local
// @@ -2,4 +2,5 @@
// entity document {
// relation owner @user
// + relation editor @user
// - permission view = owner
// + permission view = owner or editor
// }
}
```

This pairs well with `diffSchema` — use the structural diff for programmatic decisions, and the text diff for human-readable output in logs or PR comments.

## Centralized Configuration

Create a `permify.config.ts` in your root. The `defineConfig` helper makes your config compatible with CLI and NestJS:
Expand All @@ -210,21 +323,24 @@ export default defineConfig({

### Exports

| Export | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------- |
| `schema()` | Create a schema definition |
| `entity()` | Define an entity type |
| `relation()` | Define a relation on an entity |
| `attribute()` | Define an attribute on an entity |
| `permission()` | Define a permission rule |
| `defineConfig()` | Create a typed config object |
| `validateConfig()` | Validate a config object |
| `schemaFile()` | Reference a `.perm` schema file |
| `createPermifyClient()` | Create a gRPC client |
| `clientOptionsFromEnv()` | Read client options from env vars |
| `checkPermission()` | Check a permission |
| `writeRelationships()` | Write relationship tuples |
| `readRelationships()` | Read relationship tuples with filtering and automatic pagination |
| `deleteRelationships()` | Delete relationship tuples |
| `relationsOf()` | Helper to extract relations from schema |
| `getSchemaWarnings()` | Collect non-blocking warnings from a schema AST (unused relations, empty entities, missing permissions) |
| Export | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `schema()` | Create a schema definition |
| `entity()` | Define an entity type |
| `relation()` | Define a relation on an entity |
| `attribute()` | Define an attribute on an entity |
| `permission()` | Define a permission rule |
| `defineConfig()` | Create a typed config object |
| `validateConfig()` | Validate a config object |
| `schemaFile()` | Reference a `.perm` schema file |
| `createPermifyClient()` | Create a gRPC client |
| `clientOptionsFromEnv()` | Read client options from env vars |
| `checkPermission()` | Check a permission |
| `writeRelationships()` | Write relationship tuples |
| `readRelationships()` | Read relationship tuples with filtering and automatic pagination |
| `deleteRelationships()` | Delete relationship tuples |
| `relationsOf()` | Helper to extract relations from schema |
| `getSchemaWarnings()` | Collect non-blocking warnings from a schema AST (unused relations, empty entities, missing permissions) |
| `readSchemaFromPermify()` | Read the current schema from a Permify server for a given tenant |
| `diffSchema()` | Compute a structural diff between two schema entity maps |
| `textDiff()` | Generate a unified text diff between two DSL strings |
Binary file added docs-site/static/img/schema-diff-output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading