Skip to main content

Access Control

This page documents how authentication and authorization work in the Boards GraphQL API.

Overview​

The Boards API uses a layered security model:

  1. Authentication - Verifying user identity via JWT tokens
  2. Authorization - Checking permissions for specific operations
  3. Multi-tenancy - Isolating data between tenants

Authentication​

Request Headers​

Include authentication credentials in HTTP headers:

POST /graphql HTTP/1.1
Authorization: Bearer <jwt-token>
X-Tenant: <tenant-id>
Content-Type: application/json
HeaderRequiredDescription
AuthorizationYes*JWT bearer token
X-TenantDependsTenant identifier (for multi-tenant deployments)

*Required for authenticated operations. Public queries work without authentication.

JWT Token Format​

The API expects standard JWT tokens with these claims:

{
"sub": "user-unique-id",
"email": "user@example.com",
"iat": 1704067200,
"exp": 1704153600
}
ClaimDescription
subSubject - unique user identifier from auth provider
emailUser's email address
iatIssued at timestamp
expExpiration timestamp

Auth Providers​

Boards supports multiple authentication providers:

ProviderConfigurationDescription
noneBOARDS_AUTH_PROVIDER=noneNo authentication (development only)
jwtBOARDS_AUTH_PROVIDER=jwtGeneric JWT validation
supabaseBOARDS_AUTH_PROVIDER=supabaseSupabase Auth integration

See Auth Providers for detailed configuration.


Authorization​

Permission Model​

Authorization is based on user relationships to resources:

User
├── Owns Boards (owner_id)
└── Member of Boards (board_members)
└── Has Role (VIEWER, EDITOR, ADMIN)

Board Access Rules​

Access to boards is determined by:

  1. Public boards - Accessible to everyone (read-only without auth)
  2. Owner - Full access to all operations
  3. Admin member - Can manage members and edit
  4. Editor member - Can create generations
  5. Viewer member - Read-only access
def can_access_board(board, auth_context):
# Public boards are accessible to everyone
if board.is_public:
return True

# Private boards require authentication
if not auth_context.is_authenticated:
return False

# Owner has access
if board.owner_id == auth_context.user_id:
return True

# Check membership
return any(
member.user_id == auth_context.user_id
for member in board.board_members
)

Query Authorization​

QueryAuth RequiredAccess Rule
meYesReturns authenticated user
userYesAny authenticated user
boardDependsPublic or user has access
myBoardsYesOwned or member boards
publicBoardsNoAll public boards
searchBoardsYesAccessible boards only
generationDependsBoard access required
recentGenerationsYesAccessible boards only
generatorsNoPublic information

Mutation Authorization​

MutationRequired Role
createBoardAuthenticated
updateBoardOwner or Admin
deleteBoardOwner only
addBoardMemberOwner or Admin
removeBoardMemberOwner or Admin
updateBoardMemberRoleOwner or Admin
createGenerationOwner, Admin, or Editor
cancelGenerationCreator, Owner, or Admin
deleteGenerationCreator, Owner, or Admin
regenerateOwner, Admin, or Editor
uploadArtifactOwner, Admin, or Editor

Board Roles​

BoardRole Enum​

enum BoardRole {
VIEWER
EDITOR
ADMIN
}

Role Permissions​

PermissionViewerEditorAdminOwner
View boardYesYesYesYes
View generationsYesYesYesYes
Create generationsNoYesYesYes
Delete own generationsNoYesYesYes
Delete any generationNoNoYesYes
Add membersNoNoYesYes
Remove membersNoNoYesYes
Update member rolesNoNoYesYes
Update board settingsNoNoYesYes
Delete boardNoNoNoYes

Query Filters​

BoardQueryRole​

Filter boards by your relationship:

enum BoardQueryRole {
ANY # All accessible boards
OWNER # Only boards you own
MEMBER # Only boards where you're a member (not owner)
}

Example​

query GetOwnedBoards {
myBoards(role: OWNER) {
id
title
}
}

query GetSharedBoards {
myBoards(role: MEMBER) {
id
title
owner {
displayName
}
}
}

SortOrder​

Sort results by creation or update time:

enum SortOrder {
CREATED_ASC # Oldest first
CREATED_DESC # Newest first
UPDATED_ASC # Least recently updated
UPDATED_DESC # Most recently updated
}

Multi-Tenancy​

Tenant Isolation​

In multi-tenant deployments, all data is scoped to a tenant:

  • Users belong to a tenant
  • Boards belong to a tenant
  • Generations belong to a tenant

Cross-tenant access is never permitted.

Specifying Tenant​

Include the X-Tenant header:

X-Tenant: my-tenant-id

Or use a subdomain (if configured):

https://my-tenant.boards.example.com/graphql

See Multi-Tenancy for detailed configuration.


Error Responses​

Authentication Errors​

{
"errors": [
{
"message": "Not authenticated",
"path": ["myBoards"],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}

Authorization Errors​

{
"errors": [
{
"message": "You don't have permission to access this board",
"path": ["board"],
"extensions": {
"code": "FORBIDDEN"
}
}
]
}

Invalid Token​

{
"errors": [
{
"message": "Invalid or expired token",
"path": ["me"],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}

Security Best Practices​

Client-Side​

  1. Store tokens securely - Use httpOnly cookies or secure storage
  2. Handle expiration - Refresh tokens before they expire
  3. Validate responses - Check for auth errors and redirect to login

Server-Side​

  1. Validate all inputs - Never trust client data
  2. Use parameterized queries - Prevent injection attacks
  3. Log access attempts - Audit sensitive operations
  4. Rate limit - Prevent brute force attacks

Implementation Details​

Access control logic is implemented in:

  • packages/backend/src/boards/graphql/access_control.py - Core authorization functions
  • packages/backend/src/boards/auth/ - Authentication providers
  • packages/backend/src/boards/auth/middleware.py - Request middleware

Key Functions​

# Get auth context from GraphQL info
async def get_auth_context_from_info(info: strawberry.Info) -> AuthContext | None

# Check if user can access a board
def can_access_board(board: Boards, auth_context: AuthContext | None) -> bool

# Check if user is owner or member
def is_board_owner_or_member(board: Boards, auth_context: AuthContext | None) -> bool