Docs
Docs
Guides

Django

Serve every customer domain from one Django project. Replace the ALLOWED_HOSTS check with the Apexly signature and resolve the tenant from the host.

How requests arrive

Apexly forwards each visitor's request to your origin with the original Host header, X-Forwarded-Proto: https, and X-Apexly-Domain-Id, X-Apexly-App-Id, X-Apexly-Timestamp and X-Apexly-Signature.

Verify the signature

X-Apexly-Signature is the hex HMAC-SHA256 of <timestamp>.<host> keyed with your signing secret (whsec_…). This function is tested against the shared vectors on every build.

# tenants/apexly.py
import hashlib
import hmac
import re
import time


def verify_apexly_origin(host, timestamp, signature, secret, now=None) -> bool:
    """True when X-Apexly-Signature is valid for this Host (the edge signs f"{timestamp}.{host}")."""
    if not timestamp or not signature or not re.fullmatch(r"[0-9]{1,12}", timestamp):
        return False
    now = int(time.time()) if now is None else now
    if abs(now - int(timestamp)) > 300:
        return False
    name = re.sub(r":\d+$", "", host).lower()
    mac = hmac.new(secret.encode(), f"{timestamp}.{name}".encode(), hashlib.sha256)
    return hmac.compare_digest(mac.hexdigest().encode(), signature.encode())

Allow your customers' hostnames

Django refuses any host missing from ALLOWED_HOSTS with a 400. Customer hostnames are not known in advance, so let the signature decide instead: allow every host, and put a middleware first that rejects unsigned requests for hosts you don't serve yourself, before anything reads request.get_host().

# settings.py
ALLOWED_HOSTS = ["*"]  # host checking is done by ApexlyHostMiddleware
OWN_HOSTS = {"app.hireflow.com"}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
MIDDLEWARE = [
    "tenants.middleware.ApexlyHostMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # …
]
# tenants/middleware.py
import os

from django.conf import settings
from django.http import HttpResponse

from .apexly import verify_apexly_origin
from .models import Tenant


class ApexlyHostMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        host = request.META.get("HTTP_HOST", "")
        request.tenant = None
        if host.split(":")[0] not in settings.OWN_HOSTS:
            ok = verify_apexly_origin(
                host,
                request.headers.get("X-Apexly-Timestamp"),
                request.headers.get("X-Apexly-Signature"),
                os.environ["APEXLY_SIGNING_SECRET"],
            )
            if not ok:
                return HttpResponse("Unknown host", status=421)
            request.tenant = Tenant.objects.filter(hostname=host.split(":")[0].lower()).first()
        return self.get_response(request)

Forms posted on a customer domain pass CSRF checks unchanged: the Origin matches the Host the visitor used, and SECURE_PROXY_SSL_HEADER makes the request secure.

Receive webhooks

Verify Apexly-Signature against request.body with verify_apexly_signature from the Webhooks page.

# tenants/views.py
import json
import os

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def apexly_webhook(request):
    secret = os.environ["APEXLY_SIGNING_SECRET"]
    if not verify_apexly_signature(request.headers.get("Apexly-Signature"), request.body, secret):
        return HttpResponseBadRequest()
    event = json.loads(request.body)
    if event["type"] == "domain.active":
        Tenant.mark_live(event["data"]["domain"]["metadata"]["customer"])
    return HttpResponse(status=204)

Try it

Run the Quickstart with a test key, then use the sandbox origin to see the headers your app will receive.

What to read next

Was this helpful?
Edit this page