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.