workspace — Data model for organization workspaces in multi-tenant enterprise deployments
This module defines the core data models that represent a workspace: the container for an organization’s entire deployment in PocketPaw’s multi-tenant architecture. It includes Workspace (the main organizational entity with billing/licensing info) and WorkspaceSettings (configurable policies). The module exists as a separate layer to cleanly separate data persistence concerns from business logic, and serves as the contract between the database, service layer, and API routers.
Categories: data model, workspace management, multi-tenancy, MongoDB persistence
Concepts: Workspace, WorkspaceSettings, TimestampedDocument, soft delete, deleted_at, multi-tenancy, workspace scoping, slug, Indexed, unique constraint
Words: 1857 | Version: 1
Purpose
This module is the data persistence layer for workspaces — the organizational unit in PocketPaw’s multi-tenant SaaS architecture. In a multi-tenant system, one workspace = one enterprise customer or organization. Every user, agent, conversation, and data artifact belongs to exactly one workspace.
The module exists to:
- Define the schema — What data is required to represent a workspace in the database?
- Enforce constraints — Ensure workspace slugs are globally unique, define default values for settings
- Provide type safety — Give the rest of the codebase a single source of truth for workspace structure (used by
routerandservicemodules) - Enable Beanie integration — Connect to MongoDB via the Beanie ODM with proper indexing
In the larger architecture, this is a foundational domain model. Most other operations in the system are scoped by workspace: you cannot query agents or conversations without specifying which workspace they belong to. This module is the root of that scoping hierarchy.
Key Classes and Methods
WorkspaceSettings
Purpose: Encapsulates configurable policies and defaults for a workspace. Not all settings need to be set at workspace creation; they can have sensible defaults.
Fields:
default_agent: str | None— The ID of the agent that should be used by default in this workspace (e.g., when creating a new conversation without specifying an agent).Nonemeans the workspace hasn’t set a default.allow_invites: bool = True— Whether users in this workspace can invite others. Controls team expansion permissions. Defaults toTrue(open to invites) to encourage collaboration.retention_days: int | None = None— Data retention policy: how many days to keep conversation history and logs.Nonemeans keep forever (unlimited retention). Important for compliance and cost management in enterprise deployments.
Business Logic: This is a settings/configuration object, not a document. It’s embedded within a Workspace record, not stored separately. This means every workspace query returns its settings inline, avoiding extra database lookups for common configuration queries.
Workspace(TimestampedDocument)
Purpose: The core organizational entity. Represents one customer/tenant in the multi-tenant system.
Fields:
name: str— Human-readable workspace name (e.g., “Acme Corporation”). Not necessarily unique globally.slug: Indexed(str, unique=True)— URL-friendly identifier (e.g., “acme-corp”). Must be globally unique across all workspaces (enforced by MongoDB unique index). Used in URLs and programmatic references. TheIndexed(unique=True)tells Beanie to create a database index and constraint.owner: str— User ID of the admin/owner who created this workspace. This is a foreign key reference to a User document (though not explicitly enforced here). The owner typically has full permissions to delete or reconfigure the workspace.plan: str = "team"— The subscription tier/license type. Valid values are"team","business","enterprise". Determines what features are available and how many seats are granted. Sourced from the licensing system.seats: int = 5— Number of licensed user seats for this workspace. Default is 5 (suitable for small teams). Enterprise plans may have higher defaults or unlimited seats.settings: WorkspaceSettings— The embedded configuration object (see above). Defaults toWorkspaceSettings(), which gives all defaults (default_agent=None,allow_invites=True,retention_days=None).deleted_at: datetime | None = None— Soft delete marker. IfNone, the workspace is active. If set to a timestamp, the workspace is logically deleted but the record remains in the database (for audit trails, data recovery, compliance). This is a common pattern in SaaS systems to preserve data integrity.
Inherits from TimestampedDocument:
created_at: datetime— When the workspace was created (auto-set by base class)updated_at: datetime— When the workspace was last modified (auto-updated by base class)_id: PydanticObjectId— MongoDB document ID (auto-generated)
Business Logic:
- Workspace Lifecycle: A workspace starts with
deleted_at=None. When deleted, thedeleted_atfield is set but the document remains. Queries for active workspaces should filterdeleted_at=None. - Uniqueness Constraint: The slug must be unique. This is critical for multi-tenancy: if two workspaces had the same slug, URL routing would be ambiguous.
- Settings Inheritance: When a new workspace is created, it gets default settings. Users can later update
settingsto customize behavior. - Owner as Admin: The
ownerfield identifies who has initial control. Authorization logic (in theserviceor router layer) likely checks if the current user is the owner before allowing destructive operations. - Plan-Driven Limits: The
planfield gates features. Theseatsfield is typically enforced by the service layer: if you try to invite a 6th user to a team plan with 5 seats, the service rejects it.
Beanie Integration:
- Inherits from
TimestampedDocument(defined inee.cloud.models.base), which provides MongoDB document lifecycle (timestamps, ID generation). - The
class Settingsinner class withname = "workspaces"tells Beanie to store Workspace documents in the MongoDB collection namedworkspaces.
How It Works
Creation Flow:
- An API endpoint (in the
routermodule) receives a request to create a workspace (e.g., POST/workspaceswith name, plan, etc.). - The router validates the input and calls the
servicelayer. - The service layer (e.g.,
WorkspaceService) instantiates a Workspace model, sets defaults (likedeleted_at=None,settings=WorkspaceSettings()). - Beanie saves it to MongoDB. The base class auto-sets
created_atandupdated_at. MongoDB auto-generates_id. - Beanie enforces the slug uniqueness constraint: if duplicate, it raises an error (caught and returned as HTTP 409 Conflict by the router).
Retrieval Flow:
- Service queries: “Get workspace with slug=‘acme-corp’” → Beanie builds a MongoDB query and returns a Workspace instance.
- The caller gets a fully-typed Python object with all fields populated.
- The settings are already embedded, so no follow-up queries needed.
Update Flow:
- Service retrieves the workspace, modifies a field (e.g.,
workspace.plan = "enterprise"orworkspace.settings.allow_invites = False). - Calls
workspace.save()(Beanie method).updated_atis auto-updated. - MongoDB updates just the fields that changed.
Soft Delete Flow:
- Instead of deleting the document, the service sets
workspace.deleted_at = datetime.now()and callssave(). - Queries for active workspaces add a filter:
Workspace.find({"deleted_at": None}). - The document remains in the database for compliance/recovery, but is invisible to normal queries.
Edge Cases:
- Duplicate Slug: If creation tries to use an existing slug, Beanie raises a duplicate key error. The service/router should catch and return a user-friendly error.
- Settings with None: Fields like
retention_days=Noneanddefault_agent=Noneare valid. The service layer interpretsNoneas “no policy set” or “use system default”. - Plan Mismatch: If someone manually sets
plan="invalid"(not one of the three valid values), Pydantic validation doesn’t prevent it (no enum). The service layer should validate plan values. - Owner Deletion: If the user referenced in
owneris deleted, this model doesn’t cascade-delete the workspace (it’s just a string ID). The service layer must handle this scenario.
Authorization and Security
This module does not enforce authorization directly. It defines the data structure; authorization is enforced at higher layers:
- Who can view a workspace? — Anyone with access to that workspace (determined by the
routeror service via user-workspace membership checks). - Who can modify workspace settings? — Typically the owner (checked by the service before allowing updates).
- Who can delete a workspace? — Typically the owner; deletion is a soft delete (set
deleted_at). - Cross-workspace visibility: The model itself doesn’t restrict cross-workspace queries, but the service layer should always filter by workspace when querying user data (e.g., “get agents in workspace X”, not “get all agents”).
The slug: Indexed(str, unique=True) is a technical constraint (uniqueness), not an authorization control.
Dependencies and Integration
Depends On:
ee.cloud.models.base— ImportsTimestampedDocument, the base class that adds MongoDB integration,_id,created_at, andupdated_atfields.beanie— TheIndexedfunction creates database indexes and constraints. The model inherits Beanie’s document methods (save(),find(), etc.).pydantic—BaseModelandFieldprovide data validation, serialization, and field customization.WorkspaceSettingsis a plain Pydantic model (not a MongoDB document).datetime— Standard library for timestamp types (created_at,updated_at,deleted_at).
Imported By:
__init__— Re-exports Workspace and WorkspaceSettings so other modules can import from the models package cleanly (from ee.cloud.models import Workspace).router— The API layer uses Workspace to define request/response schemas and query parameters. The router calls service methods that return Workspace instances.service— The business logic layer (likelyWorkspaceService) performs CRUD operations on Workspace instances. It queries the database, validates business rules, and coordinates with other services.
System Position:
API Router (router.py) ↓ callsWorkspaceService (service.py) ↓ usesWorkspace Model (this module) + WorkspaceSettings ↓ stored inMongoDB via BeanieEvery other domain model (agents, conversations, users) likely includes a workspace_id field to establish which workspace owns the data. This module is the root.
Design Decisions
1. Embedded Settings vs. Separate Collection
- Choice:
settings: WorkspaceSettingsis embedded (a nested object), not a separate MongoDB document. - Why: Settings are small, always accessed together with the workspace, and rarely updated independently. Embedding avoids a join and keeps the data model simple.
- Trade-off: Can’t have separate permission checks on settings (e.g., “readonly user can read workspace but not settings”). Acceptable for most enterprise SaaS.
2. Soft Deletes with deleted_at
- Choice: Deletion sets
deleted_atinstead of removing the document. - Why: Preserves audit trails, enables data recovery, satisfies compliance requirements (GDPR right to erasure can be implemented as data anonymization + soft delete).
- Cost: Queries must filter
deleted_at=None. Requires discipline in the service layer.
3. Slug as Unique Identifier
- Choice:
slug: Indexed(str, unique=True)is a unique, human-readable identifier, not just the MongoDB_id. - Why: URLs and programmatic references are cleaner with “acme-corp” than with a 24-character hex ObjectId. Enables vanity URLs.
- Cost: Slugs are harder to generate safely (must avoid collisions, handle Unicode, etc.). Typically generated from the workspace name and checked for uniqueness.
4. Plan as String, Not Enum
- Choice:
plan: str = "team"is a string, not a Python enum. - Why: Flexibility — new plans can be added in the license system without updating this model. Pydantic doesn’t restrict to specific values.
- Cost: No compile-time safety. The service layer must validate that plan is one of the known values.
- Better approach: Would be
plan: Literal["team", "business", "enterprise"]for type safety, but that’s not shown here.
5. Owner as User ID String, Not Reference
- Choice:
owner: stris a string (User ID), not a foreign key or reference field. - Why: MongoDB doesn’t enforce foreign keys. Document references are intentionally loose (schema flexibility). The service layer assumes the User exists elsewhere.
- Cost: Orphaned workspaces if the owner user is deleted. The service must handle this.
6. Inheritance from TimestampedDocument
- Choice: Workspace extends
TimestampedDocument(from base.py), gainingcreated_at,updated_at,_id. - Why: Code reuse. Every document in the system needs timestamps; centralizing in a base class avoids duplication.
- Pattern: Common in MongoDB/document-DB-backed services using Beanie or similar ODMs.
7. Default Values
- Choice:
plan="team",seats=5,settings=WorkspaceSettings(),deleted_at=None,allow_invites=True. - Why: Sensible defaults reduce the chance of required-field errors. A small workspace can be created with just a name and owner.
- Business Logic: “New workspaces are team plans with 5 seats, invites enabled, and no retention limit by default.”
Related
- base-foundational-document-model-with-automatic-timestamp-management-for-mongodb
- eecloudworkspace-router-re-export-for-fastapi-workspace-endpoints
- untitled