Skip to main content

django-workspaces

PyPI - Version PyPI - Python Version

Multi-workspace support for Django. Allows users to switch between isolated workspaces within the same application, with full sync and async support.


Table of Contents


Installation

pip install django-workspaces

Quick Start

1. Add to INSTALLED_APPS and configure the middleware:

# settings.py

INSTALLED_APPS = [
    ...
    "django_workspaces",
]

MIDDLEWARE = [
    ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django_workspaces.middleware.workspace_middleware",  # after auth
]

2. Run migrations:

python manage.py migrate

3. Create workspaces and assign them to users, then use request.workspace in your views.


Configuration

All settings are optional and go in settings.py:

Setting Default Description
WORKSPACE_MODEL "django_workspaces.Workspace" Swappable workspace model — see Custom workspace model
WORKSPACE_ID_HEADER None HTTP header used to resolve the workspace by ID — see Header-based resolution
WORKSPACE_CHECK_OBJECT_PERMISSIONS False Enforce view_<model> object-level permission when entering a workspace — see Object-level permissions

Usage

Accessing the workspace in views

After adding the middleware, every request has a workspace property:

# views.py

def dashboard(request):
    workspace = request.workspace  # raises Http404 if none found
    return render(request, "dashboard.html", {"workspace": workspace})

For async views:

async def dashboard(request):
    workspace = await request.aworkspace()
    return render(request, "dashboard.html", {"workspace": workspace})

Entering and leaving workspaces

Use enter_workspace to set the active workspace for a user and leave_workspace to unset it.

from django_workspaces import enter_workspace, leave_workspace, switch_workspace

# Enter a workspace — accepts a request, an ASGI scope, or (user, workspace, session)
def select_workspace(request, workspace_id):
    workspace = get_object_or_404(Workspace, pk=workspace_id)
    enter_workspace(request, workspace=workspace)
    return redirect("dashboard")

# Leave the current workspace
def deselect_workspace(request):
    leave_workspace(request)
    return redirect("home")

# Switch directly from one workspace to another
def switch(request, workspace_id):
    workspace = get_object_or_404(Workspace, pk=workspace_id)
    switch_workspace(request, workspace=workspace)
    return redirect("dashboard")

Async equivalents are available as aenter_workspace, aleave_workspace, and aswitch_workspace:

from django_workspaces import aenter_workspace, aleave_workspace

async def select_workspace(request, workspace_id):
    workspace = await Workspace.objects.aget(pk=workspace_id)
    await aenter_workspace(request, workspace=workspace)
    return redirect("dashboard")

All three functions also accept (user, workspace, session) directly, which is useful outside of a request/response cycle:

enter_workspace(request.user, workspace=workspace, session=request.session)

Resolving a workspace manually

get_workspace resolves the active workspace from a request without the middleware:

from django_workspaces import get_workspace

def my_view(request):
    workspace = get_workspace(request)
    ...

resolve_workspace accepts a user and session directly:

from django_workspaces import resolve_workspace

workspace = resolve_workspace(user, session)

Signals

django_workspaces exposes three signals:

Signal Sent when Key arguments
workspace_requested No workspace in session; a default is being looked up user, request (optional)
workspace_entered User enters a workspace user, workspace
workspace_exited User leaves a workspace user, workspace

Setting a default workspace

Connect to workspace_requested to automatically assign a workspace when none is set in the session. The signal expects the handler to return a workspace instance (or None):

# apps.py

from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from django_workspaces.signals import workspace_requested
        workspace_requested.connect(get_default_workspace)

def get_default_workspace(sender, user, **kwargs):
    """Return the first workspace the user has access to."""
    return sender.objects.filter(members=user).first()

A common use case is persisting the last workspace a user visited, so it can be restored on their next session. Connect workspace_entered to save the preference and workspace_requested to restore it:

# myapp/models.py

class WorkspacePreference(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="workspace_preference",
    )
    last_workspace = models.ForeignKey(
        settings.WORKSPACE_MODEL,
        on_delete=models.SET_NULL,
        null=True,
    )
# myapp/apps.py

from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        from django_workspaces.signals import workspace_entered, workspace_requested
        workspace_entered.connect(save_last_workspace)
        workspace_requested.connect(restore_last_workspace)

def save_last_workspace(sender, user, workspace, **kwargs):
    """Persist the workspace the user just entered."""
    WorkspacePreference.objects.update_or_create(
        user=user,
        defaults={"last_workspace": workspace},
    )

def restore_last_workspace(sender, user, **kwargs):
    """Return the last workspace the user visited, if any."""
    pref = WorkspacePreference.objects.filter(user=user).select_related("last_workspace").first()
    return pref.last_workspace if pref else None

With this setup, the first time a user makes a request without a workspace in their session, workspace_requested fires and restore_last_workspace returns their previous workspace automatically.


Custom workspace model

To add fields to the workspace, define a custom model and point WORKSPACE_MODEL to it — similar to AUTH_USER_MODEL.

# myapp/models.py

from django_workspaces.models import AbstractWorkspace

class Project(AbstractWorkspace):
    slug = models.SlugField(unique=True)
    members = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name="projects",
    )

    class Meta(AbstractWorkspace.Meta):
        pass
# settings.py

WORKSPACE_MODEL = "myapp.Project"

Then run python manage.py makemigrations and python manage.py migrate.

Note: WORKSPACE_MODEL must be set before the first migration is run, just like AUTH_USER_MODEL.

To retrieve the active workspace model at runtime:

from django_workspaces import get_workspace_model

Workspace = get_workspace_model()

Django Channels

For WebSocket or other ASGI consumers, install the Channels extra:

pip install django-workspaces[channels]

Use WorkspaceMiddlewareStack in your ASGI routing:

# asgi.py

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from django_workspaces.contrib.channels.middleware import WorkspaceMiddlewareStack

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": WorkspaceMiddlewareStack(
        URLRouter(websocket_urlpatterns)
    ),
})

The workspace is then available on the scope:

class MyConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        workspace = self.scope["workspace"]
        ...

WorkspaceMiddlewareStack is a shortcut for AuthMiddlewareStack(WorkspaceMiddleware(inner)). If you need finer control, compose the middleware manually:

from channels.auth import AuthMiddlewareStack
from channels.sessions import SessionMiddlewareStack
from django_workspaces.contrib.channels.middleware import WorkspaceMiddleware

application = ProtocolTypeRouter({
    "websocket": SessionMiddlewareStack(
        AuthMiddlewareStack(
            WorkspaceMiddleware(
                URLRouter(websocket_urlpatterns)
            )
        )
    ),
})

Object-level permissions

Enable WORKSPACE_CHECK_OBJECT_PERMISSIONS to require users to have the view_<model> object permission before entering a workspace:

# settings.py

WORKSPACE_CHECK_OBJECT_PERMISSIONS = True

With this enabled, enter_workspace (and its async variant) raises PermissionDenied if the user does not have the view_workspace permission on the target workspace. The permission codename follows Django's convention: {app_label}.view_{model_name}.

This works with any Django-compatible permission backend, including django-guardian for row-level permissions:

from guardian.shortcuts import assign_perm

# Grant a user access to a specific workspace
assign_perm("view_workspace", user, workspace)

Header-based resolution

For API scenarios where the client specifies the workspace per request, configure WORKSPACE_ID_HEADER:

# settings.py

WORKSPACE_ID_HEADER = "x-workspace-id"

When set, get_workspace (and request.workspace) will look for this header first and resolve the workspace by its primary key. Session-based resolution is used as a fallback.

GET /api/data/ HTTP/1.1
X-Workspace-Id: 42

Note: Header-based resolution is a read-only lookup — it does not call enter_workspace internally. As a consequence, the workspace_entered and workspace_exited signals are not fired for requests that resolve the workspace through the header. If your application relies on those signals (e.g. to track the last visited workspace), prefer session-based resolution or call enter_workspace explicitly in your authentication flow.


Type hints

When using the middleware, import the enhanced request type for accurate type checking:

from django_workspaces.types import HttpRequest

def my_view(request: HttpRequest):
    workspace = request.workspace  # typed as AbstractWorkspace

License

django-workspaces is distributed under the terms of the MIT license.

Metadata

Release files for django-workspaces 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-workspaces 0.2.2
File Size Uploaded Provenance Actions
django_workspaces-0.2.2.tar.gz 19.2 kB With provenance Details for django_workspaces-0.2.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-workspaces 0.2.2
File Interpreter ABI Platform Actions
django_workspaces-0.2.2-py3-none-any.whl Python 3 none any Details for django_workspaces-0.2.2-py3-none-any.whl

Total release size: 32.3 kB

Release files / django_workspaces-0.2.2.tar.gz

Download URL django_workspaces-0.2.2.tar.gz
Size 19.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a50612dfabbadf248b34abd9bbe8e33916deb1e2524976184580d1c4ef537755
BLAKE2b-256 checksum
How to use checksums
4f8fdfc4a80ea5a5308dcb5cca9d9c4e96027634d373cc6a0a8c786caedf4646
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 24, 2026.

Transparency log

Release files / django_workspaces-0.2.2-py3-none-any.whl

Download URL django_workspaces-0.2.2-py3-none-any.whl
Size 13.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
984b05bc01cdeef45cfd8089ac5b341e34eea0b5ccc366b6c017ed5cd007e103
BLAKE2b-256 checksum
How to use checksums
a020819d88bca9e429fedf9c751e2cc16c95bd58a196327aa4d0653f4a1dfa4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 24, 2026.

Transparency log

Security for django-workspaces 0.2.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for django_workspaces-0.2.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page