Permission Object
Complete permission shape reference.
A permission object defines what a user can do with a specific table. Permissions are identified by snake_case slugs. Each permission declares which roles have access and which operations (select, insert, update, delete) are allowed — using the same patterns as Drizzle ORM.
TypeScript Interface
interface Permission {
/** Target table in connection.table format */
table: string
/** Roles that have this permission */
roles: string[]
/** Human-readable name for display in admin UI */
name?: string
/** Optional description explaining the permission's purpose */
description?: string
/** SELECT operation config */
select?: {
/** Allowed columns to return. Omit for all columns. */
columns?: string[]
/** WHERE clause — restricts which rows are visible */
where?: Record<string, any>
/** Raw SQL appended to WHERE clause */
sql?: string
/** Max rows per query */
limit?: number
/** Custom middleware wrapping query execution */
middleware?: MiddlewareFn
}
/** INSERT operation config */
insert?: {
/** Allowed columns the client can write */
columns?: string[]
/** Validate incoming values — reject with 403 if invalid */
validate?: Record<string, any>
/** Default values — used when client doesn't provide a value */
default?: Record<string, any>
/** Override values — always applied, client cannot override */
overwrite?: Record<string, any>
/** Custom middleware wrapping query execution */
middleware?: MiddlewareFn
}
/** UPDATE operation config */
update?: {
/** Allowed columns the client can write */
columns?: string[]
/** WHERE clause — restricts which rows can be updated */
where?: Record<string, any>
/** Raw SQL appended to WHERE clause */
sql?: string
/** Validate incoming values — reject with 403 if invalid */
validate?: Record<string, any>
/** Default values — used when client doesn't provide a value */
default?: Record<string, any>
/** Override values — always applied, client cannot override */
overwrite?: Record<string, any>
/** Custom middleware wrapping query execution */
middleware?: MiddlewareFn
}
/** DELETE operation config */
delete?: {
/** WHERE clause — restricts which rows can be deleted */
where?: Record<string, any>
/** Raw SQL appended to WHERE clause */
sql?: string
/** Custom middleware wrapping query execution */
middleware?: MiddlewareFn
}
}
type MiddlewareFn = (
params: { user; db; table; operation; columns; query; input?; filter? },
next: (overrides?) => Promise<any[]>,
) => Promise<any[]>Field Reference
table
| Type | string |
| Required | Yes |
| Format | connection_name.table_name |
The fully qualified table name this permission applies to. Must match a table in one of your configured connections.
table: 'main.orders'
table: 'warehouse.events'
table: 'imports.products'roles
| Type | string[] |
| Required | Yes |
Which roles have this permission. When a user authenticates and resolveSession returns their role, the engine checks if that role is listed in the permission's roles array.
roles: ['viewer', 'editor', 'admin']
roles: ['sales_rep']name
| Type | string |
| Required | No |
| Used in | Admin UI display, audit logs |
Human-readable label for the permission.
name: 'View own orders'description
| Type | string |
| Required | No |
| Used in | Admin UI tooltip, documentation |
Explains what this permission does and why it exists.
description: 'Allows users to read orders belonging to their organization. Restricted to non-financial columns.'select
| Type | SelectConfig |
| Required | No |
Configures the SELECT operation. If omitted, select is not allowed.
select: {
columns: ['id', 'amount', 'status', 'created_at'],
where: { organization_id: { $eq: '$user.current_org_id' } },
limit: 1000,
}select.columns
An allowlist of column names returned in SELECT results. Omit to allow all columns.
select: { columns: ['id', 'name', 'status'] }select.where
A condition injected into the query's WHERE clause. The user cannot see, modify, or remove this. Supports all operators and $user.* variable substitution.
// Simple equality
select: { where: { customer_id: { $eq: '$user.id' } } }
// Array membership
select: { where: { organization_id: { $in: '$user.org_ids' } } }
// Multiple conditions (AND)
select: {
where: {
organization_id: { $eq: '$user.current_org_id' },
status: { $ne: 'archived' },
},
}
// Nested relationship traversal
select: {
where: {
organization: {
members: { user_id: { $eq: '$user.id' } },
},
},
}select.sql
A raw SQL fragment appended to the WHERE clause for advanced filtering that operators cannot express.
select: {
where: { organization_id: { $in: '$user.org_ids' } },
sql: "created_at >= CURRENT_DATE - INTERVAL '30 days'",
}select.limit
Maximum rows this permission can return per query. The client's limit is capped to whichever is lower: the permission limit or the global limits.maxRows.
select: { limit: 100 }select.middleware
Custom TypeScript that wraps query execution. See Middleware.
insert
| Type | InsertConfig |
| Required | No |
Configures the INSERT operation. If omitted, insert is not allowed.
insert: {
columns: ['amount', 'status'],
validate: {
amount: { $gt: 0 },
status: { $eq: 'draft' },
},
default: {
created_at: '$now',
},
overwrite: {
created_by: '$user.id',
organization_id: '$user.current_org_id',
},
}insert.columns
An allowlist of column names the client can write. Omit to allow all columns.
insert.validate
Validation conditions the incoming data must satisfy. If the data fails, the operation is rejected with 403. Unlike where, validate checks the new data, not existing rows.
insert: {
validate: {
amount: { $gte: 0, $lte: 1_000_000 },
status: { $in: ['draft'] },
},
}insert.default
Default values applied when the client doesn't provide a value for the column. If the client sends a value, their value is used.
insert: {
default: {
created_at: '$now',
status: 'draft',
},
}insert.overwrite
Values that are always applied, regardless of what the client sends. The client cannot override these. Use for server-controlled fields like created_by or organization_id.
insert: {
overwrite: {
created_by: '$user.id',
organization_id: '$user.current_org_id',
},
}insert.middleware
Custom TypeScript that wraps query execution. See Middleware.
update
| Type | UpdateConfig |
| Required | No |
Configures the UPDATE operation. If omitted, update is not allowed.
update: {
columns: ['amount', 'status'],
where: { organization_id: { $in: '$user.org_ids' } },
validate: {
amount: { $gt: 0 },
status: { $in: ['draft', 'active', 'cancelled'] },
},
default: { updated_at: '$now' },
overwrite: { updated_by: '$user.id' },
}update.columns
An allowlist of column names the client can write.
update.where
A condition injected into the query's WHERE clause, restricting which rows can be updated.
update: {
where: { organization_id: { $in: '$user.org_ids' } },
}update.sql
A raw SQL fragment appended to the WHERE clause.
update.validate
Validation conditions the new values must satisfy.
update: {
validate: {
amount: { $gt: 0 },
status: { $in: ['draft', 'active', 'cancelled'] },
},
}update.default
Default values applied when the client doesn't provide a value for the column. If the client sends a value, their value is used.
update: {
default: { updated_at: '$now' },
}update.overwrite
Values that are always applied, regardless of what the client sends.
update: {
overwrite: { updated_by: '$user.id' },
}update.middleware
Custom TypeScript that wraps query execution. See Middleware.
delete
| Type | DeleteConfig |
| Required | No |
Configures the DELETE operation. If omitted, delete is not allowed.
delete: {
where: {
customer_id: { $eq: '$user.id' },
status: { $eq: 'draft' },
},
}delete.where
A condition injected into the query's WHERE clause, restricting which rows can be deleted.
delete.sql
A raw SQL fragment appended to the WHERE clause.
delete.middleware
Custom TypeScript that wraps query execution. See Middleware.
default vs overwrite
default | overwrite | |
|---|---|---|
| Purpose | Fill in missing values | Force server-controlled values |
| Client control | Client value wins if provided | Client value is always replaced |
| Use case | Sensible defaults, timestamps | Ownership fields, org scoping |
| Available on | insert, update | insert, update |
insert: {
// If client doesn't send created_at, use current time
default: { created_at: '$now' },
// Always set created_by to the current user, no matter what client sends
overwrite: { created_by: '$user.id' },
}Complete Examples
Read-Only Permission
view_own_orders: {
table: 'main.orders',
roles: ['viewer', 'editor', 'admin'],
description: 'Read orders belonging to the user\'s organization',
select: {
columns: ['id', 'amount', 'status', 'customer_id', 'created_at'],
where: { organization_id: { $eq: '$user.current_org_id' } },
limit: 1000,
},
}Full CRUD Permission
manage_team_tasks: {
table: 'main.tasks',
roles: ['editor', 'admin'],
description: 'Full CRUD on tasks for the user\'s team',
select: {
columns: ['id', 'title', 'description', 'status', 'priority', 'assigned_to', 'due_date'],
where: { team_id: { $in: '$user.team_ids' } },
},
insert: {
columns: ['title', 'description', 'status', 'priority', 'assigned_to', 'due_date'],
validate: {
status: { $in: ['todo', 'in_progress', 'done', 'cancelled'] },
priority: { $in: ['low', 'medium', 'high', 'critical'] },
},
overwrite: {
organization_id: '$user.current_org_id',
created_by: '$user.id',
},
},
update: {
columns: ['title', 'description', 'status', 'priority', 'assigned_to', 'due_date'],
where: { team_id: { $in: '$user.team_ids' } },
validate: {
status: { $in: ['todo', 'in_progress', 'done', 'cancelled'] },
priority: { $in: ['low', 'medium', 'high', 'critical'] },
},
overwrite: { updated_by: '$user.id' },
},
delete: {
where: { team_id: { $in: '$user.team_ids' } },
},
}Insert-Only Permission
submit_feedback: {
table: 'main.feedback',
roles: ['user'],
description: 'Users can submit feedback but cannot read or modify existing entries',
insert: {
columns: ['message', 'category', 'rating'],
validate: {
rating: { $gte: 1, $lte: 5 },
category: { $in: ['bug', 'feature', 'general'] },
},
default: { status: 'pending' },
overwrite: {
user_id: '$user.id',
submitted_at: '$now',
},
},
}Delete with Restrictions
delete_draft_orders: {
table: 'main.orders',
roles: ['sales_rep', 'admin'],
description: 'Users can only delete their own orders that are still in draft status',
delete: {
where: {
customer_id: { $eq: '$user.customer_id' },
status: { $eq: 'draft' },
},
},
}