BIO.RE
Notifications

Subscribe to Web Push

Register a W3C PushSubscription with the server. Endpoint-keyed upsert handles browser key regeneration AND device handover (different user on the same browser → reassign).

POST /api/v1/notifications/webpush/subscribe — 🔑 Bearer

Registers a W3C PushSubscription (the object returned by pushManager.subscribe() in the browser ServiceWorker). Endpoint-keyed upsert: if the same endpoint already exists in the DB, the keys are updated and (if the user changed) the row is reassigned to the new user. This handles two real-world cases — (1) the browser regenerated the keypair, (2) a different user logged in on the same browser/device.

Browser side: get a subscription first. This endpoint expects fields produced by serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: <vapid-public-key> }). The platform's VAPID public key is delivered through a separate config / env channel — clients embed it at build time. The server doesn't return it from this endpoint.

Cross-user reassignment is intentional. If the same physical device hosts two user logins over time, the browser generates one push subscription endpoint per browser profile. When user B subscribes on a browser where user A already had a subscription with the same endpoint, the row's userId is silently reassigned from A to B (with fresh p256dh / auth keys). User A no longer receives notifications on that device — they need to re-subscribe from their own profile.

Request

Body — WebPushSubscribeDto

FieldTypeRequiredValidationNotes
endpointstringIsNotEmpty()The subscription.endpoint URL (provider-specific — varies by browser/OS)
p256dhstringIsNotEmpty()Base64-encoded public key from subscription.getKey('p256dh')
authstringIsNotEmpty()Base64-encoded auth secret from subscription.getKey('auth')
userAgentstringoptionalIsString()Browser/OS UA string for support trails (helps "which device is this?")
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OK

{
  "success": true,
  "subscriptionId": "ws1a2b3c4-d5e6-7890-abcd-ef1234567890"
}
FieldTypeNotes
successbooleanAlways true on 200
subscriptionIdstring (UUID)The WebPushSubscription.id (existing-and-updated OR newly-created)

Note: the response is not wrapped in ApiResponseOf<T> — the controller returns the object directly. The shape above is what you actually get.

Errors

HTTPcode / i18nKeyReason
400(DTO validation)Missing endpoint / p256dh / auth
401(guard)Missing / invalid bearer token

Side effects

  1. prisma.webPushSubscription.findUnique({ where: { endpoint } }).
  2. Existing rowupdate({ id: existing.id, data: { userId, p256dh, auth, userAgent, active: true } }). userId is overwritten even if it differs (cross-user reassignment).
  3. New rowcreate({ id: randomUUID(), userId, endpoint, p256dh, auth, userAgent, active: true }).
  4. Audit log: [webpush] Updated/Created subscription <id> for user <userId>.
  5. Return { success: true, subscriptionId: id }.

Code samples

curl -X POST https://api.bio.re/api/v1/notifications/webpush/subscribe \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "endpoint": "https://push.example.com/abc123...",
    "p256dh": "BHV1...",
    "auth": "8Pgv...",
    "userAgent": "Mozilla/5.0 ..."
  }'
type WebPushSubscribeInput = {
  endpoint: string;
  p256dh: string;
  auth: string;
  userAgent?: string;
};

async function subscribeWebPush(
  accessToken: string,
  input: WebPushSubscribeInput,
): Promise<string> {
  const res = await fetch('https://api.bio.re/api/v1/notifications/webpush/subscribe', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(input),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw new Error(json?.error?.message ?? 'Web push subscribe failed');
  }
  return json.subscriptionId as string;
}
// 1. Register service worker (typically in app boot)
const registration = await navigator.serviceWorker.register('/sw.js');

// 2. Request notification permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
  throw new Error('Notification permission denied');
}

// 3. Subscribe to push (VAPID public key from your platform config)
const VAPID_PUBLIC_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!;

function urlBase64ToUint8Array(base64String: string): Uint8Array {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(base64);
  return Uint8Array.from(raw, (c) => c.charCodeAt(0));
}

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});

// 4. Extract keys + send to platform
function arrayBufferToBase64(buffer: ArrayBuffer | null): string {
  if (!buffer) return '';
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
  return btoa(binary);
}

const subscriptionId = await subscribeWebPush(accessToken, {
  endpoint: subscription.endpoint,
  p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
  auth: arrayBufferToBase64(subscription.getKey('auth')),
  userAgent: navigator.userAgent,
});

console.log('Web push subscription registered:', subscriptionId);

Try it

POST
/api/v1/notifications/webpush/subscribe

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

curl -X POST "https://loading/api/v1/notifications/webpush/subscribe" \  -H "Content-Type: application/json" \  -d '{}'
Empty
Empty
Empty

Source

SourcePathLines
Controllerapps/api-core/src/modules/notification/webpush.controller.ts37–53 (subscribe)
DTO (request)apps/api-core/src/modules/notification/webpush.controller.ts7–19 (WebPushSubscribeDto — inline in controller file)
Serviceapps/api-core/src/modules/notification/webpush.service.ts16–52 (subscribe — endpoint-keyed upsert with cross-user reassignment)
Prisma modelpackages/prisma/prisma/schema.prismaWebPushSubscription (@@unique([endpoint]), userId, p256dh, auth, userAgent, active)
W3C specPush APIpushManager.subscribe()

On this page