schemas — Pydantic request/response data models for workspace domain operations
This module defines the contract between the workspace API layer and its consumers by providing Pydantic data models for validating incoming requests and serializing outgoing responses. It exists to centralize workspace-related data validation and type safety in one place, ensuring consistency across the router, service layer, and external integrations (agent_bridge, ws) that need to understand workspace operations. It serves as the domain-level API boundary for all workspace CRUD, invite management, and member role operations.
Categories: workspace domain, API contract layer, data validation, schema definition, Pydantic DTOs
Concepts: CreateWorkspaceRequest, UpdateWorkspaceRequest, CreateInviteRequest, UpdateMemberRoleRequest, WorkspaceResponse, MemberResponse, InviteResponse, validate_slug, field_validator, BaseModel
Words: 1866 | Version: 1
Purpose
The schemas module is a data contract definition layer that sits between the HTTP API and the business logic. Its primary purposes are:
- Input Validation: Validates incoming HTTP requests before they reach service logic, catching malformed data early (e.g., slug format, role values)
- Type Safety: Provides structured typing through Pydantic BaseModel, enabling IDE autocomplete, static analysis, and runtime validation
- API Documentation: Serves as the source of truth for what the workspace API accepts and returns, automatically documenting endpoints
- Cross-Layer Contract: Creates a shared language between the router (HTTP layer), service layer, and external systems (agent_bridge for AI operations, ws for real-time events)
This is a stateless, declarative module — it contains no business logic, only schema definitions. It’s imported by multiple downstream consumers (router, service, group_service, message_service, ws, agent_bridge) because they all need to understand the same data structures.
Key Classes and Methods
Request Classes (Input Validation)
CreateWorkspaceRequest
Purpose: Validates the creation of a new workspace.
Fields:
name(str, 1-100 chars): The human-readable workspace nameslug(str, 1-50 chars): The URL-safe identifier for the workspace (e.g., “my-team-workspace”)
Validation Logic:
validate_slug()method enforces that slugs match the pattern^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$- Must start and end with alphanumeric characters
- Can contain hyphens in the middle
- Must be lowercase only
- This prevents invalid URLs and domain-like identifiers
Business Reason: Slugs are used in URLs (/workspace/{slug}), so they must be URL-safe and readable. Restricting to lowercase and hyphens ensures consistency across the system.
UpdateWorkspaceRequest
Purpose: Validates partial updates to an existing workspace.
Fields:
name(str | None): Optional new workspace namesettings(dict | None): Optional workspace-level configuration (flexible schema for future extensibility)
Business Reason: All fields are optional (None), allowing clients to update only what they need. This is standard REST PATCH semantics.
CreateInviteRequest
Purpose: Validates the creation of a workspace member invitation.
Fields:
email(str): The email address of the person being invitedrole(str, default=“member”): The role granted to the invitee, restricted to"admin"or"member"group_id(str | None): Optional group assignment upon joining (if workspace uses group-based organization)
Business Reason: The role field uses a strict enum pattern (^(admin|member)$) to prevent invalid role assignments. The inviter shouldn’t be able to create invites with invalid roles. Note that “owner” is NOT allowed here — ownership is likely assigned through different logic.
UpdateMemberRoleRequest
Purpose: Validates role changes for existing workspace members.
Fields:
role(str): The new role, restricted to"owner","admin", or"member"
Business Reason: Unlike CreateInviteRequest, this allows promotion to “owner”. The pattern ^(owner|admin|member)$ ensures only valid roles are accepted. This prevents typos or injection attacks that might otherwise bypass authorization checks.
Response Classes (Output Serialization)
WorkspaceResponse
Purpose: The canonical representation of a workspace returned by the API.
Fields:
id,name,slug: Core workspace identityowner(str): The ID or email of the workspace ownerplan(str): The billing plan tier (e.g., “free”, “pro”, “enterprise”) — used by downstream services to determine feature availabilityseats(int): The number of member seats available on the plancreated_at(datetime): Workspace creation timestampmember_count(int): Current number of active members (default 0 if not populated)
Usage: Returned by workspace creation, fetch, and list endpoints. The router and service layer populate this with data from the database, and it’s sent to clients and potentially to agent_bridge for AI agents to understand workspace capacity and configuration.
MemberResponse
Purpose: Represents a workspace member in API responses.
Fields:
id,email,name,avatar: Member identity and profilerole(str): The member’s current role (owner/admin/member)joined_at(datetime): When the member joined the workspace
Usage: Returned when listing workspace members or fetching member details. The avatar field allows the UI to display member pictures. The joined_at field provides audit information.
InviteResponse
Purpose: Represents a pending or accepted workspace invitation.
Fields:
id,email,role: Invitation core datainvited_by(str): The ID/email of who sent the invitation (for audit trail)token(str): The unique acceptance token (used in accept-invite endpoints, typically sent via email)accepted,revoked,expired(bool): Invitation status flagsexpires_at(datetime): When the invitation becomes invalid
Business Reason: Separating invitation state into three boolean fields (accepted, revoked, expired) makes the state machine explicit. An invitation can be revoked before expiration, or naturally expire. The token is a security credential that prevents anyone with just the email from accepting an invite.
How It Works
Data Flow
- Inbound Request: An HTTP client sends a POST to
/workspace/createwith a JSON body - Pydantic Validation: FastAPI (used by the router) automatically instantiates
CreateWorkspaceRequestfrom the JSON. If validation fails (e.g., slug has uppercase letters), Pydantic raises a validation error and FastAPI returns a 422 Unprocessable Entity response - Service Layer Call: If validation passes, the router calls the service layer with the validated request object
- Database Operation: The service layer creates the workspace in the database
- Response Serialization: The service returns data that’s mapped into
WorkspaceResponse, which FastAPI serializes to JSON - Client Receipt: The client receives the workspace details
Cross-System Usage
- router: Uses request classes to validate incoming HTTP bodies, response classes to serialize database objects
- service: Accepts request objects, uses them to validate/transform data before database operations, returns raw data that service consumers (router) serialize using response classes
- group_service, message_service: May depend on response schemas when operating within workspace scope (e.g., verifying workspace exists before creating groups/messages)
- ws (WebSocket handler): Uses response classes to serialize real-time workspace events sent to connected clients
- agent_bridge: Uses response classes to understand workspace structure and permissions when executing AI agent operations (e.g., an AI agent needs to know the
planto determine available features)
Edge Cases and Validation
- Slug Validation: The regex
^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$allows single-character slugs (second alternative) or multi-character slugs with hyphens in the middle. This prevents invalid slugs like-invalid,invalid-, orINVALID. - Optional Fields:
UpdateWorkspaceRequestandCreateInviteRequest.group_idare optional, allowing partial updates and conditional group assignment. - Role Enums: The strict pattern on role fields prevents invalid values. If a future role type is added (e.g., “editor”), all these patterns must be updated simultaneously — this is intentional to force explicit migration.
Authorization and Security
This module defines the shape of data but not the authorization logic. However, the schemas support authorization checks downstream:
- Role Pattern Restrictions: By restricting roles to known values (
admin|member|owner), the schemas prevent role injection attacks. A malicious client cannot craft a request withrole="superuser"— Pydantic will reject it. - Slug Format: The slug validation prevents directory traversal or injection attacks that might exploit URL patterns (e.g.,
/workspace/../../admin). - Token in InviteResponse: The
tokenfield is a security credential. Only the legitimate invitee who receives the email should have this token. The service layer (not this module) is responsible for validating the token matches the email before accepting an invite. - No Password Fields: Notably, these schemas don’t include passwords. Password management is likely handled in a separate auth module, which is good security practice (separation of concerns).
Authorization is enforced upstream: The router layer uses these schemas to validate format, then calls authorization middleware/decorators to check whether the requesting user is allowed to perform the operation (e.g., only workspace owners can update workspace settings).
Dependencies and Integration
Internal Dependencies
- pydantic (BaseModel, Field, field_validator): Core data validation framework. No database ORM (Beanie, SQLAlchemy) appears in this module, keeping it framework-agnostic.
- datetime: Used in
WorkspaceResponse,MemberResponse,InviteResponsefor timestamps. - re: Used for slug pattern validation.
Consumers (Inbound Dependencies)
- router (
/cloud/workspace/router.py): Uses request schemas to validate API payloads, response schemas to serialize responses. - service (
/cloud/workspace/service.py): Accepts request objects, returns data that response classes wrap. - group_service, message_service: May validate operations within workspace scope using response schemas.
- ws (WebSocket): Serializes real-time workspace events using response classes.
- agent_bridge: Deserializes
WorkspaceResponseto understand workspace configuration for AI operations.
Design Pattern: Request/Response Separation
This module uses the DTO (Data Transfer Object) pattern, split into two categories:
- Request DTOs: Validate and shape client input
- Response DTOs: Serialize and shape service output
This separation allows the service layer to accept flexible input and return rich output without coupling the HTTP contract to the database model.
Design Decisions
1. Pydantic BaseModel over Dataclasses
Pydantic was chosen (not standard dataclasses) because it provides runtime validation, serialization, and automatic OpenAPI documentation generation. Dataclasses would require manual validation logic.
2. Regex Validation for Slug
The validate_slug() method uses a custom regex pattern rather than a library-provided slug validator. This suggests:
- Explicit Control: The team wanted precise control over what constitutes a valid slug in their domain (e.g., hyphens allowed, single-char allowed).
- Documentation: The pattern is readable and self-documenting.
- No External Dependencies: Avoids a library import for a simple pattern.
3. Optional Fields in Update Requests
UpdateWorkspaceRequest uses | None syntax (Python 3.10+ union types) for all fields. This allows clients to omit fields they don’t want to change, implementing proper REST PATCH semantics.
4. Separate CreateInviteRequest and UpdateMemberRoleRequest
These could have been a single schema, but they’re separate because:
- Different Constraints: CreateInviteRequest restricts roles to
admin|member(logical: you can’t invite someone as an owner). UpdateMemberRoleRequest allowsowner|admin|member(logical: you can promote a member to owner). - Different Fields: CreateInviteRequest has
group_id; UpdateMemberRoleRequest doesn’t. - Intent Clarity: Separate classes make the intent explicit in the code and API documentation.
5. Flexible Settings Field
UpdateWorkspaceRequest.settings is typed as dict | None, not a strict schema. This suggests:
- Forward Compatibility: Settings can evolve without schema changes.
- Trade-off: Loses validation of settings structure at the schema layer. Validation is pushed to the service layer or database layer.
6. Field Defaults and Patterns
CreateInviteRequest.roledefaults to"member"— most invitations are probably member-level, so the client doesn’t need to specify it.- Role fields use
patternrather than an enum. Pydantic enums would be stricter but less flexible if roles change. Patterns are validated at serialization but allow the underlying data to be a string.
7. Explicit Boolean Flags in InviteResponse
Instead of a single status enum field (e.g., status: "pending" | "accepted" | "expired"), the schema uses three booleans: accepted, revoked, expired. This allows the database/service to represent states more flexibly (e.g., an expired invite can also be marked as revoked). The downside is that clients need to interpret multiple flags, but this is likely intentional to support complex state machines.
Architectural Notes
- Stateless and Declarative: This module has no state, no async operations, no side effects. It’s purely a declarative contract.
- Framework Agnostic (Almost): The only framework dependency is Pydantic. The schemas don’t import from FastAPI, database, or service modules, making them portable.
- Single Responsibility: Each class is focused on a single operation (Create, Update, Response), following the Single Responsibility Principle.
- Validation as a Defensive Layer: By validating at the schema layer, the downstream service and database layers can assume data is well-formed, reducing defensive programming and bugs.