Access Control

Access control defines which ShareDB operations are allowed for a collection. Access files are server-only model files and are removed from client bundles by the TeamPlay Babel plugin.

Define Rules

Access rules live in access.ts:

// models/users/access.ts
import { accessControl } from 'teamplay'

export default accessControl({
  read: ({ session }) => Boolean(session.userId),
  create: ({ session }) => Boolean(session.userId),
  update: ({ session, doc }) => session.userId === doc.id,
  delete: false
})

The object can contain four keys:

  • create: controls new document creation.
  • read: controls reading existing documents.
  • update: controls writes to existing documents.
  • delete: controls document deletion.

Each rule can be:

  • true: always allow the operation.
  • false: deny the operation.
  • omitted: deny the operation.
  • a function: decide from the operation context.
  • { fn }: ShareDB access validator object form.

Validator functions can return a boolean or a promise resolving to a boolean.

Backend Enablement

Enable access control on the backend:

import { createBackend } from 'teamplay/server'

const backend = createBackend({
  accessControl: true
})

When global access control is enabled, collections without registered rules are denied by default. This is the safe production behavior: forgetting an access.ts file does not accidentally open a collection.

When global access control is disabled, collections remain open unless they are explicitly protected by forced rules or serverOnlyCollections.

Forced Rules

Some framework-owned collections need protection even when an app has not enabled global access control yet. Mark those rules as forced:

export default accessControl({
  read: ({ session, docId }) => session.userId === docId,
  create: false,
  update: false,
  delete: false
}, { force: true })

Forced rules are registered even when createBackend({ accessControl: false }) is used. In that mode, only forced collections and server-only collections are checked; other collections keep the normal open behavior.

If global access control is enabled, forced rules behave like normal rules. The force option only controls whether the collection is protected when global access control is off.

Server-Only Collections

Use serverOnlyCollections for collections that should never be accessed by clients through ShareDB:

const backend = createBackend({
  serverOnlyCollections: ['service']
})

Server-only collections deny client read, create, update, and delete operations. Server code can still use them through server-side database access.

Rule Contexts

create receives the new document:

create: ({ type, newDoc, collection, docId, session }) => {
  return Boolean(session.userId && newDoc.name)
}

Shape:

{
  type: 'create'
  newDoc: User
  collection: string
  docId: string
  session: { userId?: string }
}

read receives the existing document:

read: ({ doc, session }) => {
  return doc.public || doc.ownerId === session.userId
}

Shape:

{
  type: 'read'
  doc: User
  collection: string
  docId: string
  session: { userId?: string }
}

update receives the document before and after the operation, plus raw ShareDB ops:

update: ({ doc, newDoc, ops, session }) => {
  return doc.ownerId === session.userId && newDoc.ownerId === doc.ownerId
}

Shape:

{
  type: 'update'
  doc: User
  newDoc: User
  ops: unknown[]
  collection: string
  docId: string
  session: { userId?: string }
}

delete receives the existing document:

delete: ({ doc, session }) => {
  return doc.ownerId === session.userId
}

Shape:

{
  type: 'delete'
  doc: User
  collection: string
  docId: string
  session: { userId?: string }
}

Document And Session Types

The first generic is the document shape. The second generic is the session shape.

import { accessControl } from 'teamplay'
import type User from './schema.ts'

interface Session {
  userId?: string
  role?: 'admin' | 'member'
}

export default accessControl<User, Session>({
  read: ({ doc, session }) => {
    return doc.public || doc.ownerId === session.userId || session.role === 'admin'
  },
  create: ({ newDoc, session }) => {
    return Boolean(session.userId && newDoc.name)
  },
  update: ({ doc, newDoc, session }) => {
    if (session.role === 'admin') return true
    return doc.ownerId === session.userId && newDoc.ownerId === doc.ownerId
  },
  delete: ({ doc, session }) => {
    return session.role === 'admin' || doc.ownerId === session.userId
  }
})

If you omit the session generic, session defaults to:

{ userId?: string }

Custom Rule Values

If your ShareDB access setup uses a custom validator that accepts extra rule values, pass the third generic:

export default accessControl<User, Session, 'admin' | 'owner'>({
  read: 'owner',
  delete: 'admin'
})

Only use this when your backend access validator is configured to understand those values.

Client Security

Access rules should stay server-only. The TeamPlay Babel plugin removes accessControl() calls from client bundles:

export default undefined

This lets client code import the same file graph without bundling authorization logic.