@djangocfg/layouts 2.1.36 → 2.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +204 -18
  2. package/package.json +5 -5
  3. package/src/components/errors/index.ts +9 -0
  4. package/src/components/errors/types.ts +38 -0
  5. package/src/layouts/AppLayout/AppLayout.tsx +33 -45
  6. package/src/layouts/AppLayout/BaseApp.tsx +105 -28
  7. package/src/layouts/AuthLayout/AuthContext.tsx +7 -1
  8. package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -10
  9. package/src/layouts/AuthLayout/OTPForm.tsx +1 -0
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  11. package/src/layouts/PublicLayout/PublicLayout.tsx +1 -1
  12. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  13. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
  14. package/src/layouts/_components/UserMenu.tsx +1 -1
  15. package/src/layouts/index.ts +1 -1
  16. package/src/layouts/types/index.ts +47 -0
  17. package/src/layouts/types/layout.types.ts +61 -0
  18. package/src/layouts/types/providers.types.ts +65 -0
  19. package/src/layouts/types/ui.types.ts +103 -0
  20. package/src/snippets/Analytics/index.ts +1 -0
  21. package/src/snippets/Analytics/types.ts +10 -0
  22. package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
  23. package/src/snippets/PWAInstall/@docs/README.md +92 -0
  24. package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +576 -0
  25. package/src/snippets/PWAInstall/README.md +185 -0
  26. package/src/snippets/PWAInstall/components/A2HSHint.tsx +227 -0
  27. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
  28. package/src/snippets/PWAInstall/components/IOSGuide.tsx +29 -0
  29. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +101 -0
  30. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +101 -0
  31. package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
  32. package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +167 -0
  33. package/src/snippets/PWAInstall/hooks/useIsPWA.ts +115 -0
  34. package/src/snippets/PWAInstall/index.ts +76 -0
  35. package/src/snippets/PWAInstall/types/components.ts +95 -0
  36. package/src/snippets/PWAInstall/types/config.ts +22 -0
  37. package/src/snippets/PWAInstall/types/index.ts +26 -0
  38. package/src/snippets/PWAInstall/types/install.ts +38 -0
  39. package/src/snippets/PWAInstall/types/platform.ts +29 -0
  40. package/src/snippets/PWAInstall/utils/localStorage.ts +181 -0
  41. package/src/snippets/PWAInstall/utils/logger.ts +149 -0
  42. package/src/snippets/PWAInstall/utils/platform.ts +151 -0
  43. package/src/snippets/PushNotifications/@docs/README.md +191 -0
  44. package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
  45. package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
  46. package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
  47. package/src/snippets/PushNotifications/README.md +328 -0
  48. package/src/snippets/PushNotifications/components/PushPrompt.tsx +165 -0
  49. package/src/snippets/PushNotifications/config.ts +20 -0
  50. package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
  51. package/src/snippets/PushNotifications/hooks/useDjangoPush.ts +259 -0
  52. package/src/snippets/PushNotifications/hooks/usePushNotifications.ts +209 -0
  53. package/src/snippets/PushNotifications/index.ts +87 -0
  54. package/src/snippets/PushNotifications/types/config.ts +28 -0
  55. package/src/snippets/PushNotifications/types/index.ts +9 -0
  56. package/src/snippets/PushNotifications/types/push.ts +21 -0
  57. package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
  58. package/src/snippets/PushNotifications/utils/logger.ts +149 -0
  59. package/src/snippets/PushNotifications/utils/platform.ts +151 -0
  60. package/src/snippets/PushNotifications/utils/vapid.ts +226 -0
  61. package/src/snippets/index.ts +55 -0
  62. package/src/layouts/shared/index.ts +0 -21
  63. package/src/layouts/shared/types.ts +0 -211
  64. /package/src/layouts/{shared → types}/README.md +0 -0
@@ -0,0 +1,648 @@
1
+ # Django Integration Guide
2
+
3
+ Complete guide to integrating PushNotifications snippet with Django-CFG backend.
4
+
5
+ ## Overview
6
+
7
+ This guide shows how to:
8
+ 1. Set up Django backend for push notifications
9
+ 2. Create subscription endpoints
10
+ 3. Send push notifications from Django
11
+ 4. Connect with DjangoPushProvider
12
+
13
+ ## Prerequisites
14
+
15
+ - ✅ Django project setup
16
+ - ✅ VAPID keys generated ([VAPID Setup Guide](./vapid-setup.md))
17
+ - ✅ Service worker configured ([Service Worker Guide](./service-worker.md))
18
+ - ✅ Python 3.8+
19
+
20
+ ## Step 1: Install Dependencies
21
+
22
+ ```bash
23
+ pip install pywebpush
24
+ ```
25
+
26
+ Update requirements:
27
+ ```bash
28
+ pip freeze > requirements.txt
29
+ ```
30
+
31
+ ## Step 2: Configure Django Settings
32
+
33
+ ### settings.py
34
+
35
+ ```python
36
+ # settings.py
37
+ import os
38
+ from pathlib import Path
39
+
40
+ # VAPID Configuration
41
+ VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY')
42
+ VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY')
43
+ VAPID_MAILTO = os.getenv('VAPID_MAILTO', 'mailto:admin@example.com')
44
+
45
+ # Validate VAPID keys (development)
46
+ if DEBUG and not all([VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY]):
47
+ import warnings
48
+ warnings.warn(
49
+ "VAPID keys not configured. "
50
+ "Run: npx web-push generate-vapid-keys"
51
+ )
52
+
53
+ # CORS (if frontend on different domain)
54
+ CORS_ALLOWED_ORIGINS = [
55
+ 'http://localhost:3000', # Next.js dev
56
+ 'https://your-domain.com',
57
+ ]
58
+
59
+ CORS_ALLOW_CREDENTIALS = True
60
+ ```
61
+
62
+ ### Environment Variables
63
+
64
+ ```bash
65
+ # .env
66
+ VAPID_PUBLIC_KEY=BM8xT...
67
+ VAPID_PRIVATE_KEY=abc123...
68
+ VAPID_MAILTO=mailto:your-email@example.com
69
+ ```
70
+
71
+ ## Step 3: Create Push Models
72
+
73
+ ### models.py
74
+
75
+ ```python
76
+ # apps/notifications/models.py
77
+ from django.db import models
78
+ from django.contrib.auth.models import User
79
+ from django.utils import timezone
80
+
81
+ class PushSubscription(models.Model):
82
+ """Store user push subscriptions"""
83
+
84
+ user = models.ForeignKey(
85
+ User,
86
+ on_delete=models.CASCADE,
87
+ related_name='push_subscriptions'
88
+ )
89
+
90
+ endpoint = models.URLField(max_length=500, unique=True)
91
+ p256dh = models.CharField(max_length=255) # Public key
92
+ auth = models.CharField(max_length=255) # Auth secret
93
+
94
+ created_at = models.DateTimeField(auto_now_add=True)
95
+ updated_at = models.DateTimeField(auto_now=True)
96
+ is_active = models.BooleanField(default=True)
97
+
98
+ # Browser info (optional)
99
+ user_agent = models.TextField(blank=True)
100
+ ip_address = models.GenericIPAddressField(null=True, blank=True)
101
+
102
+ class Meta:
103
+ db_table = 'push_subscriptions'
104
+ ordering = ['-created_at']
105
+ indexes = [
106
+ models.Index(fields=['user', 'is_active']),
107
+ models.Index(fields=['endpoint']),
108
+ ]
109
+
110
+ def __str__(self):
111
+ return f"{self.user.username} - {self.endpoint[:50]}..."
112
+
113
+ @property
114
+ def subscription_info(self):
115
+ """Format for pywebpush"""
116
+ return {
117
+ "endpoint": self.endpoint,
118
+ "keys": {
119
+ "p256dh": self.p256dh,
120
+ "auth": self.auth
121
+ }
122
+ }
123
+
124
+
125
+ class PushNotification(models.Model):
126
+ """Log sent notifications"""
127
+
128
+ subscription = models.ForeignKey(
129
+ PushSubscription,
130
+ on_delete=models.CASCADE,
131
+ related_name='notifications'
132
+ )
133
+
134
+ title = models.CharField(max_length=255)
135
+ body = models.TextField()
136
+ icon = models.URLField(blank=True)
137
+ url = models.URLField(blank=True)
138
+
139
+ sent_at = models.DateTimeField(auto_now_add=True)
140
+ success = models.BooleanField(default=True)
141
+ error_message = models.TextField(blank=True)
142
+
143
+ class Meta:
144
+ db_table = 'push_notifications'
145
+ ordering = ['-sent_at']
146
+
147
+ def __str__(self):
148
+ return f"{self.title} - {self.sent_at}"
149
+ ```
150
+
151
+ ### Migrate
152
+
153
+ ```bash
154
+ python manage.py makemigrations
155
+ python manage.py migrate
156
+ ```
157
+
158
+ ## Step 4: Create Views
159
+
160
+ ### views.py
161
+
162
+ ```python
163
+ # apps/notifications/views.py
164
+ from django.http import JsonResponse
165
+ from django.views.decorators.csrf import csrf_exempt
166
+ from django.views.decorators.http import require_http_methods
167
+ from django.contrib.auth.decorators import login_required
168
+ from django.conf import settings
169
+ import json
170
+
171
+ from .models import PushSubscription
172
+ from .utils import send_push_notification
173
+
174
+
175
+ @csrf_exempt # Remove in production, use CSRF token
176
+ @require_http_methods(["POST"])
177
+ @login_required
178
+ def subscribe(request):
179
+ """
180
+ Subscribe user to push notifications
181
+
182
+ Expects JSON body:
183
+ {
184
+ "endpoint": "https://...",
185
+ "keys": {
186
+ "p256dh": "...",
187
+ "auth": "..."
188
+ }
189
+ }
190
+ """
191
+ try:
192
+ data = json.loads(request.body)
193
+
194
+ # Validate data
195
+ if not all([
196
+ data.get('endpoint'),
197
+ data.get('keys', {}).get('p256dh'),
198
+ data.get('keys', {}).get('auth')
199
+ ]):
200
+ return JsonResponse({
201
+ 'success': False,
202
+ 'error': 'Invalid subscription data'
203
+ }, status=400)
204
+
205
+ # Create or update subscription
206
+ subscription, created = PushSubscription.objects.update_or_create(
207
+ endpoint=data['endpoint'],
208
+ defaults={
209
+ 'user': request.user,
210
+ 'p256dh': data['keys']['p256dh'],
211
+ 'auth': data['keys']['auth'],
212
+ 'is_active': True,
213
+ 'user_agent': request.META.get('HTTP_USER_AGENT', ''),
214
+ 'ip_address': request.META.get('REMOTE_ADDR'),
215
+ }
216
+ )
217
+
218
+ return JsonResponse({
219
+ 'success': True,
220
+ 'created': created,
221
+ 'subscriptionId': subscription.id
222
+ })
223
+
224
+ except json.JSONDecodeError:
225
+ return JsonResponse({
226
+ 'success': False,
227
+ 'error': 'Invalid JSON'
228
+ }, status=400)
229
+ except Exception as e:
230
+ return JsonResponse({
231
+ 'success': False,
232
+ 'error': str(e)
233
+ }, status=500)
234
+
235
+
236
+ @csrf_exempt
237
+ @require_http_methods(["POST"])
238
+ @login_required
239
+ def unsubscribe(request):
240
+ """Unsubscribe from push notifications"""
241
+ try:
242
+ data = json.loads(request.body)
243
+ endpoint = data.get('endpoint')
244
+
245
+ if not endpoint:
246
+ return JsonResponse({
247
+ 'success': False,
248
+ 'error': 'Endpoint required'
249
+ }, status=400)
250
+
251
+ # Deactivate subscription
252
+ updated = PushSubscription.objects.filter(
253
+ endpoint=endpoint,
254
+ user=request.user
255
+ ).update(is_active=False)
256
+
257
+ return JsonResponse({
258
+ 'success': True,
259
+ 'unsubscribed': updated > 0
260
+ })
261
+
262
+ except Exception as e:
263
+ return JsonResponse({
264
+ 'success': False,
265
+ 'error': str(e)
266
+ }, status=500)
267
+
268
+
269
+ @require_http_methods(["POST"])
270
+ @login_required
271
+ def send_test_push(request):
272
+ """Send test notification to user"""
273
+ try:
274
+ data = json.loads(request.body)
275
+
276
+ # Get user's active subscriptions
277
+ subscriptions = PushSubscription.objects.filter(
278
+ user=request.user,
279
+ is_active=True
280
+ )
281
+
282
+ if not subscriptions.exists():
283
+ return JsonResponse({
284
+ 'success': False,
285
+ 'error': 'No active subscriptions'
286
+ }, status=400)
287
+
288
+ # Notification data
289
+ notification = {
290
+ 'title': data.get('title', 'Test Notification'),
291
+ 'body': data.get('body', 'This is a test from Django'),
292
+ 'icon': data.get('icon', '/icon-192.png'),
293
+ 'url': data.get('url', '/'),
294
+ }
295
+
296
+ # Send to all subscriptions
297
+ results = []
298
+ for subscription in subscriptions:
299
+ success = send_push_notification(
300
+ subscription.subscription_info,
301
+ notification
302
+ )
303
+ results.append({
304
+ 'subscriptionId': subscription.id,
305
+ 'success': success
306
+ })
307
+
308
+ return JsonResponse({
309
+ 'success': True,
310
+ 'results': results
311
+ })
312
+
313
+ except Exception as e:
314
+ return JsonResponse({
315
+ 'success': False,
316
+ 'error': str(e)
317
+ }, status=500)
318
+ ```
319
+
320
+ ### utils.py
321
+
322
+ ```python
323
+ # apps/notifications/utils.py
324
+ from pywebpush import webpush, WebPushException
325
+ from django.conf import settings
326
+ import json
327
+ import logging
328
+
329
+ logger = logging.getLogger(__name__)
330
+
331
+
332
+ def send_push_notification(subscription_info, notification_data):
333
+ """
334
+ Send push notification using pywebpush
335
+
336
+ Args:
337
+ subscription_info: dict with 'endpoint' and 'keys'
338
+ notification_data: dict with 'title', 'body', 'icon', etc.
339
+
340
+ Returns:
341
+ bool: True if successful, False otherwise
342
+ """
343
+ try:
344
+ # Send push
345
+ response = webpush(
346
+ subscription_info=subscription_info,
347
+ data=json.dumps(notification_data),
348
+ vapid_private_key=settings.VAPID_PRIVATE_KEY,
349
+ vapid_claims={
350
+ "sub": settings.VAPID_MAILTO
351
+ }
352
+ )
353
+
354
+ logger.info(f"Push sent successfully: {notification_data.get('title')}")
355
+ return True
356
+
357
+ except WebPushException as e:
358
+ logger.error(f"Push failed: {e}")
359
+
360
+ # Handle specific errors
361
+ if e.response and e.response.status_code == 410:
362
+ # Subscription expired - deactivate it
363
+ logger.warning(f"Subscription expired: {subscription_info['endpoint']}")
364
+
365
+ return False
366
+
367
+ except Exception as e:
368
+ logger.error(f"Unexpected error sending push: {e}")
369
+ return False
370
+ ```
371
+
372
+ ## Step 5: Configure URLs
373
+
374
+ ### urls.py
375
+
376
+ ```python
377
+ # apps/notifications/urls.py
378
+ from django.urls import path
379
+ from . import views
380
+
381
+ app_name = 'notifications'
382
+
383
+ urlpatterns = [
384
+ path('push/subscribe/', views.subscribe, name='push_subscribe'),
385
+ path('push/unsubscribe/', views.unsubscribe, name='push_unsubscribe'),
386
+ path('push/test/', views.send_test_push, name='push_test'),
387
+ ]
388
+ ```
389
+
390
+ ### Main urls.py
391
+
392
+ ```python
393
+ # project/urls.py
394
+ from django.urls import path, include
395
+
396
+ urlpatterns = [
397
+ # ... other patterns
398
+ path('api/', include('apps.notifications.urls')),
399
+ ]
400
+ ```
401
+
402
+ ## Step 6: Configure Frontend
403
+
404
+ ### Using DjangoPushProvider
405
+
406
+ ```tsx
407
+ // app/layout.tsx
408
+ import { DjangoPushProvider, PushPrompt } from '@/snippets/PushNotifications';
409
+
410
+ const VAPID_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!;
411
+
412
+ export default function Layout({ children }) {
413
+ return (
414
+ <DjangoPushProvider
415
+ vapidPublicKey={VAPID_KEY}
416
+ autoSubscribe={false}
417
+ onSubscribed={(sub) => console.log('Subscribed:', sub)}
418
+ onSubscribeError={(err) => console.error('Error:', err)}
419
+ >
420
+ {children}
421
+ <PushPrompt requirePWA={true} />
422
+ </DjangoPushProvider>
423
+ );
424
+ }
425
+ ```
426
+
427
+ ### Send Test Notification
428
+
429
+ ```tsx
430
+ // components/TestPushButton.tsx
431
+ 'use client';
432
+
433
+ import { useDjangoPushContext } from '@/snippets/PushNotifications';
434
+
435
+ export function TestPushButton() {
436
+ const { sendTestPush, isSubscribed } = useDjangoPushContext();
437
+
438
+ if (!isSubscribed) {
439
+ return null;
440
+ }
441
+
442
+ const handleTest = async () => {
443
+ const success = await sendTestPush({
444
+ title: 'Test from Frontend',
445
+ body: 'This is a test notification',
446
+ url: '/dashboard',
447
+ });
448
+
449
+ if (success) {
450
+ alert('Test notification sent!');
451
+ } else {
452
+ alert('Failed to send notification');
453
+ }
454
+ };
455
+
456
+ return (
457
+ <button onClick={handleTest}>
458
+ Send Test Notification
459
+ </button>
460
+ );
461
+ }
462
+ ```
463
+
464
+ ## Step 7: Admin Integration (Optional)
465
+
466
+ ### admin.py
467
+
468
+ ```python
469
+ # apps/notifications/admin.py
470
+ from django.contrib import admin
471
+ from .models import PushSubscription, PushNotification
472
+
473
+
474
+ @admin.register(PushSubscription)
475
+ class PushSubscriptionAdmin(admin.ModelAdmin):
476
+ list_display = ['user', 'endpoint_short', 'is_active', 'created_at']
477
+ list_filter = ['is_active', 'created_at']
478
+ search_fields = ['user__username', 'endpoint']
479
+ readonly_fields = ['created_at', 'updated_at']
480
+
481
+ def endpoint_short(self, obj):
482
+ return obj.endpoint[:50] + '...'
483
+ endpoint_short.short_description = 'Endpoint'
484
+
485
+
486
+ @admin.register(PushNotification)
487
+ class PushNotificationAdmin(admin.ModelAdmin):
488
+ list_display = ['title', 'subscription', 'success', 'sent_at']
489
+ list_filter = ['success', 'sent_at']
490
+ search_fields = ['title', 'body']
491
+ readonly_fields = ['sent_at']
492
+ ```
493
+
494
+ ## Step 8: Testing
495
+
496
+ ### Test Flow
497
+
498
+ ```bash
499
+ # 1. Start Django
500
+ python manage.py runserver
501
+
502
+ # 2. Start Next.js
503
+ npm run dev
504
+
505
+ # 3. Open browser
506
+ # Navigate to http://localhost:3000
507
+
508
+ # 4. Subscribe to push
509
+ # Click "Enable Notifications" button
510
+
511
+ # 5. Send test notification
512
+ # Click "Test" button or use Django admin
513
+ ```
514
+
515
+ ### Django Shell Testing
516
+
517
+ ```python
518
+ python manage.py shell
519
+
520
+ >>> from apps.notifications.models import PushSubscription
521
+ >>> from apps.notifications.utils import send_push_notification
522
+
523
+ >>> # Get user subscription
524
+ >>> sub = PushSubscription.objects.filter(is_active=True).first()
525
+
526
+ >>> # Send test
527
+ >>> send_push_notification(
528
+ ... sub.subscription_info,
529
+ ... {
530
+ ... 'title': 'Test from Shell',
531
+ ... 'body': 'Testing push',
532
+ ... 'url': '/dashboard'
533
+ ... }
534
+ ... )
535
+ ```
536
+
537
+ ## Production Considerations
538
+
539
+ ### Security
540
+
541
+ ```python
542
+ # Remove @csrf_exempt in production
543
+ # Use Django's CSRF protection
544
+
545
+ from django.views.decorators.csrf import ensure_csrf_cookie
546
+
547
+ @ensure_csrf_cookie
548
+ @require_http_methods(["POST"])
549
+ @login_required
550
+ def subscribe(request):
551
+ # ... implementation
552
+ ```
553
+
554
+ ### Rate Limiting
555
+
556
+ ```python
557
+ from django.core.cache import cache
558
+ from django.http import JsonResponse
559
+
560
+ def rate_limit(request, key, max_requests=10, window=60):
561
+ """Simple rate limiting"""
562
+ cache_key = f'rate_limit:{key}:{request.user.id}'
563
+ current = cache.get(cache_key, 0)
564
+
565
+ if current >= max_requests:
566
+ return JsonResponse({
567
+ 'error': 'Rate limit exceeded'
568
+ }, status=429)
569
+
570
+ cache.set(cache_key, current + 1, window)
571
+ return None
572
+
573
+ @login_required
574
+ def send_test_push(request):
575
+ # Check rate limit
576
+ rate_limit_response = rate_limit(request, 'push_test', max_requests=5, window=60)
577
+ if rate_limit_response:
578
+ return rate_limit_response
579
+
580
+ # ... rest of implementation
581
+ ```
582
+
583
+ ### Logging
584
+
585
+ ```python
586
+ # settings.py
587
+ LOGGING = {
588
+ 'version': 1,
589
+ 'disable_existing_loggers': False,
590
+ 'handlers': {
591
+ 'file': {
592
+ 'level': 'INFO',
593
+ 'class': 'logging.FileHandler',
594
+ 'filename': 'push_notifications.log',
595
+ },
596
+ },
597
+ 'loggers': {
598
+ 'apps.notifications': {
599
+ 'handlers': ['file'],
600
+ 'level': 'INFO',
601
+ 'propagate': True,
602
+ },
603
+ },
604
+ }
605
+ ```
606
+
607
+ ## Troubleshooting
608
+
609
+ ### Issue: Subscription fails
610
+
611
+ **Check**:
612
+ 1. CSRF token (if not using `@csrf_exempt`)
613
+ 2. User authentication
614
+ 3. CORS settings
615
+ 4. VAPID keys match
616
+
617
+ ### Issue: Push not received
618
+
619
+ **Check**:
620
+ 1. Subscription is active in database
621
+ 2. Service worker registered
622
+ 3. Notification permission granted
623
+ 4. VAPID keys correct on backend
624
+
625
+ ### Issue: 410 Gone error
626
+
627
+ **Solution**: Subscription expired, remove from database
628
+
629
+ ```python
630
+ # Handle in utils.py
631
+ if e.response and e.response.status_code == 410:
632
+ from .models import PushSubscription
633
+ PushSubscription.objects.filter(
634
+ endpoint=subscription_info['endpoint']
635
+ ).update(is_active=False)
636
+ ```
637
+
638
+ ## Next Steps
639
+
640
+ - [VAPID Setup](./vapid-setup.md) - Configure VAPID keys
641
+ - [Service Worker](./service-worker.md) - Configure SW
642
+ - [Main README](../../README.md) - API reference
643
+
644
+ ## References
645
+
646
+ - [pywebpush Documentation](https://github.com/web-push-libs/pywebpush)
647
+ - [Django REST Framework](https://www.django-rest-framework.org/)
648
+ - [Web Push Protocol](https://datatracker.ietf.org/doc/html/rfc8030)