superapp
ReferenceServer API

Permission Object

Complete permission shape reference.

Chat in Claude

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

Typestring
RequiredYes
Formatconnection_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

Typestring[]
RequiredYes

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

Typestring
RequiredNo
Used inAdmin UI display, audit logs

Human-readable label for the permission.

name: 'View own orders'

description

Typestring
RequiredNo
Used inAdmin 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

TypeSelectConfig
RequiredNo

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

TypeInsertConfig
RequiredNo

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

TypeUpdateConfig
RequiredNo

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

TypeDeleteConfig
RequiredNo

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

defaultoverwrite
PurposeFill in missing valuesForce server-controlled values
Client controlClient value wins if providedClient value is always replaced
Use caseSensible defaults, timestampsOwnership fields, org scoping
Available oninsert, updateinsert, 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' },
    },
  },
}

On this page