Django Clerk Users
Integrate Clerk authentication with Django.
Production note: Pin versions and review release notes before upgrading between minor versions.
Features
- Custom user model (
ClerkUser) with Clerk integration - JWT token validation via Clerk SDK
- Session-based authentication middleware (validates once, caches in session)
- Webhook handling with Svix signature verification
- Optional organizations support (separate sub-app)
- Django REST Framework authentication (optional)
- Server-side Clerk SDK helpers for account provisioning and invite links
Installation
pip install django-clerk-users
For Django REST Framework support:
pip install django-clerk-users[drf]
To use the official clerk-backend-api SDK for server-side API calls instead
of the built-in thin client (note that this reintroduces that SDK's
cryptography<49 pin):
pip install django-clerk-users[sdk]
Use .env.example as a starting point for required Clerk settings in each
environment.
Compatibility
This package supports Python 3.12 through 3.14 and Django 4.2, 5.2, and 6.0. CI installs and tests the built wheel across those supported Django/Python combinations and exercises the optional Django REST Framework extra.
cryptography is declared as >=45 with no upper bound, and a default
install now has no ceiling in practice either. clerk-backend-api pins
cryptography<49, and that pin is what resolvers obey, so it lives in the
optional [sdk] extra rather than the base dependencies:
pip install django-clerk-users # cryptography 50.x
pip install django-clerk-users[sdk] # held to cryptography 48.x
A scheduled workflow reports where the upstream ceiling sits, and a
py313-cryptolatest tox environment runs the suite against the newest
cryptography:
uv run python scripts/check_cryptography_ceiling.py
uv run tox -e py313-cryptolatest
Clerk API client backends
Server-side Clerk API calls go through a client selected by
CLERK_CLIENT_BACKEND:
| Value | Client | Requires | cryptography |
|---|---|---|---|
"thin" (default) |
Built-in REST client | nothing extra | unbounded |
"sdk" |
Official clerk-backend-api |
[sdk] extra |
capped at <49 |
# settings.py — only needed to opt into the official SDK
CLERK_CLIENT_BACKEND = "sdk"
Selection is by setting only, never by what happens to be installed.
get_clerk_client() is public API, so picking an implementation based on
importability would make response models, error types, retries, and available
methods depend on the environment, and would silently restore the
cryptography ceiling for anyone who acquired clerk-backend-api as a
transitive dependency. Setting CLERK_CLIENT_BACKEND = "sdk" without
installing the extra raises ClerkConfigurationError rather than falling back.
Session token verification never uses the SDK on either backend.
Quick Start
1. Add to installed apps
INSTALLED_APPS = [
# ...
"django_clerk_users",
# Optional: for organization support
# "django_clerk_users.organizations",
]
2. Configure settings
# Required
CLERK_SECRET_KEY = "sk_live_..." # From Clerk Dashboard
CLERK_WEBHOOK_SIGNING_KEY = "whsec_..." # From Clerk Webhooks
CLERK_FRONTEND_HOSTS = ["https://your-app.com"] # Your frontend URLs
# Optional
CLERK_SESSION_REVALIDATION_SECONDS = 300 # Re-validate JWT every 5 minutes
CLERK_CACHE_TIMEOUT = 300 # Cache timeout for user lookups
3. Set the user model
AUTH_USER_MODEL = "django_clerk_users.ClerkUser"
Or extend the abstract model for custom fields:
# myapp/models.py
from django.db import models
from django_clerk_users.models import AbstractClerkUser
class CustomUser(AbstractClerkUser):
company = models.CharField(max_length=255, blank=True)
class Meta(AbstractClerkUser.Meta):
swappable = "AUTH_USER_MODEL"
# settings.py
AUTH_USER_MODEL = "myapp.CustomUser"
4. Add middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_clerk_users.middleware.ClerkAuthMiddleware", # Add after AuthenticationMiddleware
# ...
]
5. Add authentication backend
For Clerk-only authentication:
AUTHENTICATION_BACKENDS = [
"django_clerk_users.authentication.ClerkBackend",
]
For hybrid authentication (Clerk + Django admin):
If you want to support both Clerk authentication (JWT) and traditional Django admin login (username/password), use both backends:
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", # For Django admin
"django_clerk_users.authentication.ClerkBackend", # For Clerk JWT
]
This allows:
- Admin users to log in via Django admin with username/password
- Frontend users to authenticate via Clerk JWT tokens
- The middleware automatically detects which authentication method was used
6. Run migrations
python manage.py migrate
7. Configure webhooks
Add the webhook URL to your urls.py:
from django_clerk_users.webhooks import clerk_webhook_view
urlpatterns = [
# ...
path("webhooks/clerk/", clerk_webhook_view, name="clerk_webhook"),
]
Then configure your Clerk Dashboard to send webhooks to https://your-app.com/webhooks/clerk/.
For additional Clerk webhook endpoints, each endpoint gets its own Svix signing secret. Use the package verifier with an endpoint-specific setting:
from django.http import JsonResponse
from django_clerk_users.webhooks import clerk_webhook_required
@clerk_webhook_required(signing_key_setting="CLERK_ACTIVATION_WEBHOOK_SIGNING_KEY")
def activation_webhook(request):
data = request.clerk_webhook_data
return JsonResponse({"ok": True, "type": data.get("type")})
8. Create admin users (for hybrid authentication)
If you're using hybrid authentication, create an admin user for Django admin access:
python manage.py createsuperuser
This creates a user with:
- Username/password authentication (for Django admin)
- No
clerk_id(since they're not Clerk users) - Access to Django admin panel
Note: Regular Clerk users are created automatically via webhooks when they sign up through your frontend.
Usage
Accessing the user in views
def my_view(request):
if request.user.is_authenticated:
# Access Clerk user attributes
print(request.user.clerk_id)
print(request.user.email)
print(request.user.full_name)
# Access organization (if using organizations)
print(request.org) # Organization ID from JWT
Decorators
from django_clerk_users.decorators import clerk_user_required
@clerk_user_required
def protected_view(request):
# Only authenticated Clerk users can access
return HttpResponse(f"Hello, {request.user.email}")
Django REST Framework
# settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"django_clerk_users.authentication.ClerkAuthentication",
],
}
For hybrid APIs that accept Clerk bearer tokens and Django session users, use the combined authenticator:
# settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"django_clerk_users.authentication.ClerkSessionAuthentication",
],
}
ClerkSessionAuthentication checks Authorization: Bearer ... with Clerk first.
Requests without a bearer token fall back to Django session authentication.
Bearer token failures are not hidden by the session fallback.
Server-side Clerk operations
For backend flows that create accounts outside Clerk's hosted sign-up UI, use the server API helpers instead of hand-writing Clerk SDK calls in each Django app:
from django_clerk_users.server_api import provision_clerk_user_access_link
result = provision_clerk_user_access_link(
"invitee@example.com",
"https://app.example.com/sign-in",
first_name="Ada",
public_metadata={"invite_type": "staff"},
expires_in_seconds=7 * 24 * 3600,
auto_username=True, # useful when the Clerk instance requires usernames
)
if result["access_link"]:
send_invite_email(result["access_link"])
The helpers cover common server-side tasks:
create_clerk_user()creates passwordless or password-backed Clerk users.create_clerk_sign_in_token()andcreate_clerk_sign_in_link()mint one-time access links using Clerk's__clerk_ticketflow.get_clerk_user_by_email(),set_clerk_user_email(), andupdate_clerk_user_public_metadata()keep Django profile workflows in sync with Clerk.revoke_clerk_user_sessions(),send_clerk_invitation(), andrevoke_clerk_invitation()wrap common account-management operations.
When CLERK_SECRET_KEY is missing or set to a documented local placeholder
(abc123, sk_test_mock_secret_key, or sk_live_replace_me), creation helpers
return {"no_key": True} and mutation helpers no-op with a falsey result.
Hybrid Authentication (Clerk + Django Admin)
The package supports hybrid authentication, allowing you to use both Clerk (JWT-based) authentication for your frontend users and traditional Django admin authentication for internal staff.
How it works
- Frontend users: Authenticate via Clerk JWT tokens (handled by
ClerkAuthMiddleware) - Admin users: Authenticate via username/password (handled by Django's
ModelBackend) - The middleware automatically detects which authentication method was used and respects existing sessions
Configuration
# settings.py
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", # For Django admin
"django_clerk_users.authentication.ClerkBackend", # For Clerk JWT
]
Creating admin users
Admin users don't need a clerk_id (it's optional in hybrid mode):
python manage.py createsuperuser
# Email: admin@example.com
# Password: ********
This creates a user with:
- Username/password authentication (no Clerk integration)
- Access to Django admin panel at
/admin/ - Standard Django permissions (is_staff, is_superuser)
Session handling
- Django admin sessions: Traditional session cookies (set by Django's auth system)
- Clerk sessions: JWT validated once, then cached in session with
last_clerk_checkmarker - The middleware checks for
last_clerk_checkto distinguish between the two types
Use cases
This is particularly useful when:
- Your admin panel is on a different domain than your frontend
- You want internal staff to access Django admin without Clerk accounts
- You need traditional Django auth features (permissions, groups, etc.)
- You're migrating from Django auth to Clerk gradually
Claim and conversion flows
Some Django apps pre-create users before a Clerk signup exists. For example, a student, invitee, or imported account may later claim a real Clerk identity. If Clerk signs the person in before your app finishes the claim, the standard user sync can create a fresh duplicate user for the claimed email.
Use absorb_clerk_user_duplicate() at the point where your app has verified the
claim and knows the historical target user:
from django_clerk_users.utils import absorb_clerk_user_duplicate
def duplicate_is_fresh_shell(user):
return (
not user.is_staff
and not user.is_superuser
and not user.has_usable_password()
# Add app-specific checks here, e.g. no memberships, roles, orders, etc.
)
absorb_clerk_user_duplicate(
target_user,
email=claimed_email,
safe_to_delete=duplicate_is_fresh_shell,
)
target_user.email = claimed_email
target_user.save(update_fields=["email"])
The helper moves the duplicate's clerk_id to the target user, deletes the
duplicate by default, and invalidates affected Clerk user cache entries. It raises
ClerkUserMergeConflictError instead of deleting when the duplicate is not safe
to absorb.
Organizations (Optional)
For Clerk organization support:
# settings.py
INSTALLED_APPS = [
# ...
"django_clerk_users",
"django_clerk_users.organizations",
]
MIDDLEWARE = [
# ...
"django_clerk_users.middleware.ClerkAuthMiddleware",
"django_clerk_users.organizations.middleware.ClerkOrganizationMiddleware",
]
Management Commands
# Sync users from Clerk
python manage.py sync_clerk_users
# Sync organizations from Clerk
python manage.py sync_clerk_organizations
# Preview migration of existing Django users into Clerk
python manage.py migrate_users_to_clerk --source-model auth.User --all --dry-run --skip-existing
# Migrate a bounded batch and link local rows when matching Clerk users already exist
python manage.py migrate_users_to_clerk --source-model auth.User --all --limit 100 --skip-existing
migrate_users_to_clerk creates passwordless Clerk users because Django password
hashes cannot be migrated into Clerk. Existing local users with a clerk_id
field are linked to matching Clerk users when --skip-existing is used, and the
command recovers duplicate-email create races by looking up and linking the
existing Clerk user. --created-before YYYY-MM-DD filters on date_joined for
classic Django users and created_at for ClerkUser-based source models.
Production Checklist
- Set
CLERK_SECRET_KEY,CLERK_WEBHOOK_SIGNING_KEY, andCLERK_FRONTEND_HOSTSfrom environment variables or a secret manager. - Serve webhook endpoints over HTTPS and keep Svix signature verification enabled. Use a separate signing secret for each custom webhook endpoint.
- Put
ClerkAuthMiddlewareafter Django'sAuthenticationMiddleware, and putClerkOrganizationMiddlewareafterClerkAuthMiddlewarewhen using organizations. - Run
python manage.py check --deployin CI or deployment validation; the package registers checks for placeholder secrets, missing frontend host allowlists, and middleware ordering. - Configure application logging for
django_clerk_users.*so authentication, webhook, and sync failures are visible in production. - Run
python manage.py migrateduring deploys and run sync commands with--dry-runbefore backfilling existing Clerk data. - Keep the package's CI gates enabled: lockfile checks, Ruff, formatting, migrations, tox across supported Python versions, build verification, and coverage threshold enforcement.
Release validation
Before publishing a release, run the artifact and installed-wheel checks:
uv build
uv run python scripts/check_dist.py
uv run --no-sync python -m pip install --force-reinstall --no-deps dist/django_clerk_users-*.whl
uv run --no-sync python scripts/smoke_installed_wheel.py
Then run the read-only live Clerk smoke check against a test/development Clerk instance:
export CLERK_SECRET_KEY=sk_test_...
export CLERK_WEBHOOK_SIGNING_KEY=whsec_...
export CLERK_FRONTEND_HOSTS=https://app.example.com
# Optional: make the smoke check verify a known existing user lookup.
export CLERK_LIVE_SMOKE_LOOKUP_EMAIL=user@example.com
uv run python scripts/live_clerk_smoke.py
scripts/live_clerk_smoke.py performs no writes. It runs Django system checks,
calls Clerk's user list endpoint, optionally looks up an existing user by email,
and verifies a signed Svix webhook payload through the package verifier.
The tag-based release workflow runs these same artifact checks and the live
Clerk smoke check before publishing to PyPI. Configure the release environment
with CLERK_SECRET_KEY and CLERK_WEBHOOK_SIGNING_KEY secrets, plus optional
CLERK_FRONTEND_HOSTS and CLERK_LIVE_SMOKE_LOOKUP_EMAIL variables.
Use the Live Clerk Smoke workflow to run the same installed-wheel and live
Clerk checks manually before tagging a release.
Auto-Generated Usernames
Clerk doesn't require usernames, but Django often does (for admin, URLs, etc.). This package provides options for generating usernames automatically.
Synchronous Generation
Generate usernames inline during user creation:
# settings.py
CLERK_AUTO_GENERATE_USERNAME = True # Enable auto-generation
CLERK_AUTO_GENERATE_USERNAME_PREFIX = "user" # Optional, default is "user"
Usernames are generated as {prefix}_{uuid8} (e.g., user_abc12345).
Async Generation (Celery, django-qstash, etc.)
For high-traffic apps, you may want to defer username generation to a background task. Keep CLERK_AUTO_GENERATE_USERNAME disabled (the default) and use the clerk_user_created signal to trigger your async task:
# myapp/signals.py
from django.dispatch import receiver
from django_clerk_users.webhooks.signals import clerk_user_created
@receiver(clerk_user_created)
def handle_user_created(sender, user, clerk_data, **kwargs):
from myapp.tasks import generate_username_task
generate_username_task.delay(user.pk)
# myapp/tasks.py (Celery example)
from celery import shared_task
from django_clerk_users.utils import generate_username_for_user
@shared_task
def generate_username_task(user_id: int):
return generate_username_for_user(user_id)
Backfilling Existing Users
To generate usernames for existing users without one:
from django_clerk_users.utils import generate_usernames_for_users_without
# Synchronous backfill
count = generate_usernames_for_users_without()
# With custom prefix
count = generate_usernames_for_users_without(prefix="member")
Password Sync
When you change a user's password in Django, it can automatically sync to Clerk:
# Sync password to both Django and Clerk (default)
user.set_password("new_password")
user.save()
# Django only - skip Clerk sync
user.set_password("new_password", sync_to_clerk=False)
user.save()
Notes:
- Sync is enabled by default (
sync_to_clerk=True) - Users without a
clerk_id(e.g., Django admin users) skip Clerk sync automatically - Clerk API errors are logged but don't prevent the Django password from being set
Disable global password sync when Django passwords are only for local session users or staff accounts:
# settings.py
CLERK_SYNC_PASSWORDS = False
Existing projects can also use the legacy opt-out flag:
CLERK_DISABLE_PASSWORD_SYNC = True
Configuration Reference
| Setting | Required | Default | Description |
|---|---|---|---|
CLERK_SECRET_KEY |
Yes | - | Your Clerk secret key |
CLERK_WEBHOOK_SIGNING_KEY |
Yes* | - | Webhook signing secret (*required for webhooks) |
CLERK_FRONTEND_HOSTS |
Yes | [] |
Authorized frontend URLs |
CLERK_AUTH_PARTIES |
No | [] |
Alias for CLERK_FRONTEND_HOSTS |
CLERK_JWT_KEY |
No | - | PEM public key for networkless token verification (skips the JWKS request) |
CLERK_CLIENT_BACKEND |
No | "thin" |
Clerk API client: "thin" (built-in) or "sdk" (requires the [sdk] extra) |
CLERK_SESSION_REVALIDATION_SECONDS |
No | 300 |
JWT revalidation interval (seconds) |
CLERK_CACHE_TIMEOUT |
No | 300 |
User cache timeout (seconds) |
CLERK_ORG_CACHE_TIMEOUT |
No | 900 |
Organization cache timeout (seconds) |
CLERK_API_TIMEOUT_MS |
No | 10000 |
Timeout for server-side Clerk SDK helper calls |
CLERK_WEBHOOK_DEDUP_TIMEOUT |
No | 45 |
Webhook deduplication cache timeout (seconds) |
CLERK_AUTO_GENERATE_USERNAME |
No | False |
Auto-generate usernames synchronously |
CLERK_AUTO_GENERATE_USERNAME_PREFIX |
No | "user" |
Prefix for auto-generated usernames |
CLERK_SYNC_PASSWORDS |
No | True |
Sync Django password changes to Clerk when a user has clerk_id |
CLERK_DISABLE_PASSWORD_SYNC |
No | False |
Legacy opt-out; disables password sync when True |
Comma-separated strings are accepted for CLERK_FRONTEND_HOSTS and
CLERK_AUTH_PARTIES. Numeric settings may be provided as strings. Boolean
settings accept common environment values such as true, false, 1, and 0.
License
MIT
Contributing
Contributions are welcome! Please open an issue or PR on GitHub.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_clerk_users-0.4.0.tar.gz.
File metadata
- Download URL: django_clerk_users-0.4.0.tar.gz
- Upload date:
- Size: 141.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3172d2258d8aa72fb9cd0a8366923beae864edd0aa7476ab8addfcfc8f4695ac
|
|
| MD5 |
e6b719c72b57b63b357c08429e88372a
|
|
| BLAKE2b-256 |
b831e23b9aa63621f8dc1596d935ad55800e99fc4549d16643b7e42029187e2f
|
Provenance
The following attestation bundles were made for django_clerk_users-0.4.0.tar.gz:
Publisher:
release.yaml on jmitchel3/django-clerk-users
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_clerk_users-0.4.0.tar.gz -
Subject digest:
3172d2258d8aa72fb9cd0a8366923beae864edd0aa7476ab8addfcfc8f4695ac - Sigstore transparency entry: 2341216772
- Sigstore integration time:
-
Permalink:
jmitchel3/django-clerk-users@1854778083f568c8a22da50643c2975224b39e43 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/jmitchel3
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@1854778083f568c8a22da50643c2975224b39e43 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_clerk_users-0.4.0-py3-none-any.whl.
File metadata
- Download URL: django_clerk_users-0.4.0-py3-none-any.whl
- Upload date:
- Size: 94.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12b7c28d6fa08b8d915891ca13bcfe88655ae69ec80384951b19f5db921893fb
|
|
| MD5 |
20ba4bd0d4c4827cdea2b9f32c783f18
|
|
| BLAKE2b-256 |
169e67ed765f5fd40a7d5303ac6dd018a2d79089c3d9980e59fcbcdb1cc8138f
|
Provenance
The following attestation bundles were made for django_clerk_users-0.4.0-py3-none-any.whl:
Publisher:
release.yaml on jmitchel3/django-clerk-users
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_clerk_users-0.4.0-py3-none-any.whl -
Subject digest:
12b7c28d6fa08b8d915891ca13bcfe88655ae69ec80384951b19f5db921893fb - Sigstore transparency entry: 2341216787
- Sigstore integration time:
-
Permalink:
jmitchel3/django-clerk-users@1854778083f568c8a22da50643c2975224b39e43 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/jmitchel3
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@1854778083f568c8a22da50643c2975224b39e43 -
Trigger Event:
push
-
Statement type: