Migrate from Pusher to Vask (Django). Drop-in for pusher-http-python.

Vask speaks the Pusher protocol, so a Django app using pusher-http-python and pusher-js usually migrates by changing credentials, host, and the frontend bundle.

Migration summary

At a glance

low risk
Type
Drop-in host swap
Typical time
10-15 minutes
Server SDK
pusher-http-python unchanged
Client SDK
pusher-js unchanged
Auth changes
reuse /pusher/auth/; pass Django CSRF header
Rollback
restore Pusher env values and redeploy bundle

Files touched

  • myproject/pusher_client.py
  • .env or deployment secrets
  • client Pusher module
  • myproject/views.py

Get started

Realtime made simple.

Free Tier: 500K broadcasts/mo and 100 concurrent across unlimited apps. $10/mo when you outgrow it.

Minimal config diff

Set the Vask app key as both PUSHER_APP_ID and PUSHER_APP_KEY. If your SDK asks for an app id, use the Vask app key. Vask does not issue a separate customer-facing app id.

- PUSHER_APP_ID=123456
- PUSHER_APP_KEY=abc123
- PUSHER_APP_SECRET=xyz789
- PUSHER_APP_CLUSTER=us3
+ PUSHER_APP_ID=your_vask_key
+ PUSHER_APP_KEY=your_vask_key
+ PUSHER_APP_SECRET=your_vask_secret
+ PUSHER_HOST=wss.vask.dev
+ PUSHER_PORT=443
+ PUSHER_APP_CLUSTER=mt1

Keep one server-side client and add the explicit host:

import os
from pusher import Pusher

pusher_client = Pusher(
    app_id=os.environ["PUSHER_APP_ID"],
    key=os.environ["PUSHER_APP_KEY"],
    secret=os.environ["PUSHER_APP_SECRET"],
    host=os.environ.get("PUSHER_HOST", "wss.vask.dev"),
    port=int(os.environ.get("PUSHER_PORT", 443)),
    ssl=True,
    cluster=os.environ.get("PUSHER_APP_CLUSTER", "mt1"),
)

Rollback

Restore the old Pusher environment values, remove or override PUSHER_HOST, redeploy the frontend bundle with your normal pipeline, and restart Django workers. No package rollback is required.

What changes / what stays

  • Changes: host, key, secret, optional compatibility cluster, and any compiled client config.
  • Stays: pusher_client.trigger, channel names, event names, pusher-js, auth signatures, and any existing Django URL for channel auth.

Server publish

Keep existing trigger calls:

from myproject.pusher_client import pusher_client

pusher_client.trigger("orders", "order.created", {"id": order.id})

Instantiate the client once in settings.py or a small pusher_client.py module. Per-view construction works, but it makes cutover verification noisier.

Client subscribe

Expose only browser-safe values, then point pusher-js at Vask:

import Pusher from 'pusher-js';

function getCookie(name) {
    const match = document.cookie.match(
        new RegExp('(^| )' + name + '=([^;]+)'),
    );

    return match ? decodeURIComponent(match[2]) : null;
}

const pusher = new Pusher(window.VASK_PUSHER.key, {
    cluster: window.VASK_PUSHER.cluster || 'mt1',
    wsHost: window.VASK_PUSHER.host || 'wss.vask.dev',
    wsPort: 443,
    wssPort: 443,
    forceTLS: true,
    enabledTransports: ['ws', 'wss'],
    authEndpoint: '/pusher/auth/',
    auth: {
        headers: {
            'X-CSRFToken': getCookie('csrftoken'),
        },
    },
});

Do not expose PUSHER_APP_SECRET to templates, Vite, Webpack, or any public window object.

Private and presence channels

The auth view still signs the same Pusher-compatible payload:

from django.http import HttpResponseForbidden, JsonResponse
from django.views.decorators.http import require_POST
from myproject.pusher_client import pusher_client


@require_POST
def pusher_auth(request):
    if not request.user.is_authenticated:
        return HttpResponseForbidden()

    auth = pusher_client.authenticate(
        channel=request.POST["channel_name"],
        socket_id=request.POST["socket_id"],
        custom_data={
            "user_id": str(request.user.id),
            "user_info": {"name": request.user.get_username()},
        },
    )

    return JsonResponse(auth)

Prefer sending X-CSRFToken from the browser. If you mark the view csrf_exempt, keep session or token authorization inside the view.

Verify

  1. Deploy or restart Django with the new environment.
  2. Hard refresh the browser and confirm a WebSocket connects to wss.vask.dev.
  3. Trigger pusher_client.trigger from a view, Celery task, or python manage.py shell.
  4. Test one public channel and one private or presence channel before production cutover.

Gotchas

  • CSRF 403s. Confirm the csrftoken cookie exists, the X-CSRFToken header is sent, and request.user.is_authenticated is true.
  • Channels still mounted. ASGI Channels routes can coexist with Vask during migration. Remove them later only if no clients use them.
  • Bundle cache still has Pusher. Rebuild or redeploy the client bundle and hard refresh if the browser still connects to a Pusher host.
  • Cluster expectations. Vask routes by PUSHER_HOST, not cluster. Keep mt1 only for SDK compatibility.

Where to go next

Does pusher-http-python work unchanged?
Yes. Keep the package and trigger calls. Set host to wss.vask.dev, use your Vask app key and secret. If your SDK asks for an app id, use the Vask app key.
Can I keep CSRF protection on the auth view?
Yes. Pass the csrftoken cookie through pusher-js as X-CSRFToken. Only use csrf_exempt if that is already your accepted pattern for channel auth.
Do I need to replace Django Channels?
No. Channels with a Redis layer can stay if it serves the app. This guide is for Django apps already using pusher-http-python, or apps intentionally moving to the hosted Pusher protocol.
How do I roll back if auth or sockets fail?
Restore the old Pusher environment values, clear or redeploy the frontend bundle, and restart the Django process. The server package and browser SDK stay the same.

Get going

Vask keeps the Django migration at the credential layer: same package, same client, same auth view, new host.

Make the switch

Drop in your Vask credentials. Keep your package.

pusher-http-python on the server, pusher-js on the client, the same auth callback at /pusher/auth/. Most teams ship the cutover in around fifteen minutes.