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
- 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=mt1Keep 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
- Deploy or restart Django with the new environment.
- Hard refresh the browser and confirm a WebSocket connects to
wss.vask.dev. - Trigger
pusher_client.triggerfrom a view, Celery task, orpython manage.py shell. - Test one public channel and one private or presence channel before production cutover.
Gotchas
- CSRF 403s. Confirm the
csrftokencookie exists, theX-CSRFTokenheader is sent, andrequest.user.is_authenticatedis 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. Keepmt1only for SDK compatibility.
Where to go next
- Run the Pusher fan-out calculator against your workload.
- Read the Pusher vs Vask comparison for pricing and protocol detail.
- Check the Django guide for advanced setup.
- If another service shares events, compare the Laravel and Rails recipes.
- 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.