Quickback Docs

Views - Column Level Security

Views implement Column Level Security (CLS) - controlling which columns users can access. This complements Row Level Security (RLS) provided by the Firewall, which controls which rows users can access.

Security LayerControlsQuickback Feature
Row Level SecurityWhich recordsFirewall
Column Level SecurityWhich fieldsViews

Views provide named field projections with role-based access control. Use views to return different sets of columns to different users without duplicating CRUD endpoints.

When to Use Views

ConceptPurposeExample
CRUD listFull recordsReturns all columns
MaskingHide valuesSSN *****6789
ViewsExclude columnsOnly id, name, status
  • CRUD list returns all fields to authorized users
  • Masking transforms sensitive values but still includes the column
  • Views completely exclude columns from the response

Basic Usage

// features/customers/customers.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { defineTable } from '@quickback/compiler';

export const customers = sqliteTable('customers', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull(),
  phone: text('phone'),
  ssn: text('ssn'),
  internalNotes: text('internal_notes'),
  organizationId: text('organization_id').notNull(),
});

export default defineTable(customers, {
  masking: {
    ssn: { type: 'ssn', show: { roles: ['admin'] } },
  },

  crud: {
    list: { access: { roles: ['owner', 'admin', 'member'] } },
    get: { access: { roles: ['owner', 'admin', 'member'] } },
  },

  // Views: Named field projections
  views: {
    summary: {
      fields: ['id', 'name', 'email'],
      access: { roles: ['owner', 'admin', 'member'] },
    },
    full: {
      fields: ['id', 'name', 'email', 'phone', 'ssn', 'internalNotes'],
      access: { roles: ['admin'] },
    },
    report: {
      fields: ['id', 'name', 'email', 'createdAt'],
      access: { roles: ['finance', 'admin'] },
    },
  },
});

Generated Endpoints

Views generate GET endpoints at /{resource}/views/{viewName}:

GET /api/v1/customers                    # CRUD list - all fields
GET /api/v1/customers/views/summary      # View - only summary fields
GET /api/v1/customers/views/full         # View - all fields (admin only)
GET /api/v1/customers/views/report       # View - report fields

Query Parameters

Views support the same query parameters as the list endpoint:

Pagination

ParameterDescriptionDefaultMax
limitNumber of records to return50100
offsetNumber of records to skip0-
# Get first 10 records
GET /api/v1/customers/views/summary?limit=10

# Get records 11-20
GET /api/v1/customers/views/summary?limit=10&offset=10

Filtering

# Filter by exact value
GET /api/v1/customers/views/summary?status=active

# Filter with operators
GET /api/v1/customers/views/summary?createdAt.gt=2024-01-01

# Multiple filters (AND logic)
GET /api/v1/customers/views/summary?status=active&email.like=yourdomain.com

Sorting

ParameterDescriptionDefault
sortField to sort bycreatedAt
orderSort direction (asc or desc)desc
GET /api/v1/customers/views/summary?sort=name&order=asc

Security

All four security pillars apply to views:

PillarBehavior
FirewallWHERE clause applied (same as list)
AccessPer-view access control
GuardsN/A (read-only)
MaskingApplied to returned fields

Firewall

Views automatically apply the same firewall conditions as the list endpoint. Users only see records within their organization scope.

Access Control

Each view has its own access configuration:

views: {
  // Public view - available to all members
  summary: {
    fields: ['id', 'name', 'status'],
    access: { roles: ['owner', 'admin', 'member'] },
  },
  // Restricted view - admin only
  full: {
    fields: ['id', 'name', 'status', 'ssn', 'internalNotes'],
    access: { roles: ['admin'] },
  },
}

Masking

Masking rules are applied to the returned fields. If a view includes a masked field like ssn, the masking rules still apply:

masking: {
  ssn: { type: 'ssn', show: { roles: ['admin'] } },
},

views: {
  // Even if a member accesses the 'full' view, ssn will be masked
  // because masking rules take precedence
  full: {
    fields: ['id', 'name', 'ssn'],
    access: { roles: ['owner', 'admin', 'member'] },
  },
}

How Views and Masking Work Together

Views and masking are orthogonal concerns:

  • Views control field selection (which columns appear)
  • Masking controls field transformation (how values appear based on role)

Example configuration:

masking: {
  ssn: { type: 'ssn', show: { roles: ['admin'] } },
},

views: {
  summary: {
    fields: ['id', 'name'],  // ssn NOT included
    access: { roles: ['owner', 'admin', 'member'] },
  },
  full: {
    fields: ['id', 'name', 'ssn'],  // ssn included
    access: { roles: ['owner', 'admin', 'member'] },
  },
}
EndpointRolessn in response?ssn value
/views/summarymemberNoN/A
/views/summaryadminNoN/A
/views/fullmemberYes*****6789
/views/fulladminYes123-45-6789

Response Format

View responses include metadata about the view:

{
  "data": [
    { "id": "cust_123", "name": "John Doe", "email": "john@yourdomain.com" },
    { "id": "cust_456", "name": "Jane Smith", "email": "jane@yourdomain.com" }
  ],
  "view": "summary",
  "fields": ["id", "name", "email"],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "count": 2
  }
}

Complete Example

// features/employees/employees.ts
export default defineTable(employees, {
  guards: {
    createable: ['name', 'email', 'phone', 'department'],
    updatable: ['name', 'email', 'phone'],
  },

  masking: {
    ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },
    salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },
    personalEmail: { type: 'email', show: { or: 'owner' } },
  },

  crud: {
    list: { access: { roles: ['owner', 'admin', 'member'] } },
    get: { access: { roles: ['owner', 'admin', 'member'] } },
    create: { access: { roles: ['owner', 'admin'] } },
    update: { access: { roles: ['owner', 'admin'] } },
    delete: { access: { roles: ['owner', 'admin'] } },
  },

  views: {
    // Directory view - public employee info
    directory: {
      fields: ['id', 'name', 'email', 'department'],
      access: { roles: ['owner', 'admin', 'member'] },
    },
    // HR view - includes sensitive info
    hr: {
      fields: ['id', 'name', 'email', 'phone', 'ssn', 'salary', 'department', 'startDate'],
      access: { roles: ['owner', 'admin'] },
    },
    // Payroll export
    payroll: {
      fields: ['id', 'name', 'ssn', 'salary', 'bankAccount'],
      access: { roles: ['owner', 'admin'] },
    },
  },
});

On this page