Access control
FFDB exposes access rules through the client so your app can make smart UI and query decisions before a user hits denied operations. This page shows how to read permissions and why each part matters.
Why this matters
Application access control is not only a backend concern. Reading access context on the client helps you:
- Name
Hide invalid actions early- Description
Disable or hide buttons for operations the current user cannot perform.
- Name
Prevent invalid query shapes- Description
Respect column and pagination constraints before sending a query.
- Name
Explain behavior clearly- Description
Show users why actions are unavailable instead of failing silently.
Access interface shape
type TableAccess = {
table: string
access: {
read: boolean
insert: boolean
update: boolean
delete: boolean
}
constraints: {
allowedReadColumns?: string[]
deniedReadColumns?: string[]
writableColumns?: string[]
requiredPredicateColumns?: string[]
maxLimit?: number
maxOffset?: number
allowOffset?: boolean
}
explicitlyConfigured: boolean
adminOverride: boolean
}
type AccessInfo = {
userId: string
role: string
blockedStatementTypes: string[]
tables: TableAccess[]
}
End-to-end access flow
Use getAccess at app startup or after auth changes, then map permissions into UI and query logic.
import { createClient } from 'ffdb-client'
import type { Database } from './ffdb.types'
const { getAccess, db } = await createClient<Database>({
config: { apiUrl: import.meta.env.VITE_FFDB_API_URL },
})
const access = await getAccess()
const userTable = access.tables.find((t) => t.table === 'user')
if (!userTable?.access.read) {
throw new Error('Current user cannot read user table')
}
const selectedColumns =
userTable.constraints.allowedReadColumns ?? ['id', 'email', 'name']
const limit = Math.min(25, userTable.constraints.maxLimit ?? 25)
const rows = await db
.selectFrom('user')
.select(selectedColumns as Array<'id' | 'email' | 'name'>)
.limit(limit)
.execute()
console.log(rows)
UI gating pattern
This is a practical pattern for forms and actions.
function UserActions({ access }: { access: AccessInfo }) {
const userTable = access.tables.find((t) => t.table === 'user')
const canCreate = Boolean(userTable?.access.insert)
const canEdit = Boolean(userTable?.access.update)
const canDelete = Boolean(userTable?.access.delete)
return (
<div>
<button disabled={!canCreate}>Create user</button>
<button disabled={!canEdit}>Edit user</button>
<button disabled={!canDelete}>Delete user</button>
</div>
)
}
Why gate in UI if the backend still enforces rules? Better UX. Users see available actions immediately and avoid avoidable failures.
Constraint-aware query guidance
- Name
allowedReadColumns / deniedReadColumns- Description
Build select lists from allowed columns where possible.
- Name
writableColumns- Description
Restrict edit forms and patch payload builders to writable fields.
- Name
requiredPredicateColumns- Description
Ensure filters include required columns for scoped reads.
- Name
maxLimit / maxOffset / allowOffset- Description
Clamp pagination controls to policy-allowed ranges.
- Name
blockedStatementTypes- Description
Avoid exposing unavailable operation classes in power-user workflows.
Practical tradeoffs
- Caching access info reduces repeated checks, but refresh it after sign-in, sign-out, and role changes.
- Strict client-side gating improves UX, but never replace backend enforcement with client checks.
- Dynamic query shaping gives flexibility, but define safe fallbacks when constraints are missing.