django-workspaces
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
- Quick Start
- Configuration
- Usage
- Signals
- Custom workspace model
- Django Channels
- Object-level permissions
- Header-based resolution
- Type hints
- License
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_MODELmust be set before the first migration is run, just likeAUTH_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_workspaceinternally. As a consequence, theworkspace_enteredandworkspace_exitedsignals 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 callenter_workspaceexplicitly 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.