Files, email and push notifications - storing, sending and fanning out
How files live in object storage behind one interface and travel as streams, how signed links and virus scanning work, how an email travels with SMTP and MIME and reaches the inbox (SPF, DKIM, DMARC, one-click unsubscribe), how Web Push with VAPID and a service worker reaches a phone, and how one event fans out to many people while storing them only as ids.
In short
Three jobs sit at the edge of almost every web product: keeping files, sending email and reaching a phone with a push notification. Each one talks to a system outside the product - a storage service, a mail provider, a browser's push service. {{brand}} puts each behind one small interface and chooses the vendor by configuration, so features never change when the vendor does. On top sits a notification pipeline: one event becomes one row per person, grouped, filtered by their preferences, and delivered to the inbox, a device or an email digest.
After this lesson you can Design file storage that moves from local disk to Azure Blob Storage or S3 without touching a feature, explain SMTP, MIME, SPF, DKIM and DMARC to a colleague, send a Web Push message with VAPID and a service worker from scratch, and design a notification fan-out with grouping, digests and preferences that stores people only as ids.
Words you need
- Object storage A service that keeps files as objects: a key, the bytes and a little metadata, read and written over HTTP. Azure Blob Storage and Amazon S3 are the best known.
- Magic bytes The first bytes of a file that show what it really is, whatever its name or label says.
- Stream Reading or writing bytes piece by piece, so a 2 GB video never has to sit in memory at once.
- Signed URL A link that carries its own signature and expiry, so storage serves one file to whoever holds it for a few minutes. Azure calls it a SAS, S3 a pre-signed URL.
- ClamAV and clamd The open-source antivirus engine. clamd is its background service: you send it bytes over a socket and it answers OK or names the threat.
- SMTP Simple Mail Transfer Protocol: the text protocol mail servers use to hand a message to each other.
- MIME The format that lets one email carry plain text, HTML and attachments, each as its own part.
- SPF, DKIM and DMARC Three DNS records that prove a message comes from your domain: who may send, a cryptographic signature, and what a receiver does when a check fails.
- Web Push The browser standard for notifications sent by a server, even when the site is closed, delivered through the browser maker's push service. A TTL says how long that service keeps trying to deliver.
- VAPID Voluntary Application Server Identification: a short signed token that tells the push service which server is sending.
- Service worker A script the browser runs in the background for one site. It receives the push and shows the notification.
- Fan-out and digest Fan-out turns one event into one delivery per recipient. A digest collects many events over time and sends one summary.
Files: one interface, three places to keep them
A feature that stores a file should not care where the bytes go. {{brand}} gives every module the same tiny contract: save a stream and get a key back, open a key, delete a key. An adapter behind it talks to the real storage. The module keeps only the key, next to its own row with the file name, size and type.
- 1. Check the size Each endpoint has a body limit. A request that declares a larger body is answered 413 Payload Too Large before a byte is read.
- 2. Check the record May this person attach a file to this record? Is there still room in their quota?
- 3. Read the first bytes The first few dozen bytes show what the file really is - see Trust the bytes below.
- 4. Scan The bytes are streamed to the virus scanner; an infected file stops here.
- 5. Save The adapter streams the bytes into storage under a new random key.
- 6. Write the row File name, type, size and key are saved. If that save fails, the file is deleted again, so no orphan is left behind.
Never use the uploaded name as the key A name such as ../../app.config or invoice.pdf.exe is chosen by the uploader. Store the bytes under a random key with at most a short, safe extension, keep the real name in the database, and give it back only in the Content-Disposition header of a download.
Uploads and downloads as streams
A server that reads a whole upload into memory falls over when a few people upload large files at once. Streaming means the bytes flow through in small buffers: from the network, through the scanner, into storage - and back the same way on download. Memory use stays flat whatever the file size.
- The browser sends multipart/form-data The file travels as one part of a multipart body, next to any form fields, each part separated by a boundary line.
- The server reads the form In ASP.NET Core small parts stay in memory and larger ones are spooled to a temporary file, so a big upload does not fill memory.
- One stream, passed along The same stream is read in pieces by the scanner, rewound, and read again by the storage adapter. A stream that cannot rewind is first copied to a temporary file.
- The download The stored object is opened as a stream and copied straight into the response. With range requests a video player asks for one slice and gets 206 Partial Content, so it can seek.
How an email travels: SMTP, email APIs and MIME
Your server never delivers an email to someone's inbox itself. It hands the message to a mail provider, which signs it, sends it to the receiver's mail server and deals with refusals. The hand-off is either SMTP, the old text protocol every mail system speaks, or an email API over HTTPS.
- 1. Build the message From, to, subject, a plain-text part and an HTML part, sometimes attachments - assembled as MIME.
- 2. Hand it over Over SMTP to a relay (port 587 with STARTTLS, or 465 with TLS from the first byte), or over an HTTPS email API such as Amazon SES or Postmark.
- 3. The provider signs and sends It adds a DKIM signature and delivers to the receiver's mail server, found through the domain's MX record, over SMTP.
- 4. The receiver judges It checks SPF, DKIM and DMARC, the sender's reputation and the content.
- 5. Inbox, spam or bounce Accepted into the inbox or the spam folder, or refused with a bounce. You see which in the provider's dashboard and logs.
How Web Push works, step by step
Three parties take part: your server, the push service run by the browser maker (Google's FCM for Chrome, Mozilla's for Firefox, Apple's for Safari, Microsoft's for Edge on Windows) and the service worker in the person's browser. Your server never connects to the phone. It posts an encrypted message to the push service, which holds it until the device is online.
- 1. Register a service worker The page registers a script, for example /sw.js. The browser can wake it even when the tab is closed.
- 2. Ask permission at the right moment Call Notification.requestPermission() only after the person presses a button - never on page load. A refusal is close to permanent.
- 3. Subscribe pushManager.subscribe with userVisibleOnly: true and your VAPID public key as the applicationServerKey.
- 4. The browser answers A subscription: an endpoint URL at its push service, plus two keys - p256dh (the browser's public key) and auth (a 16-byte secret).
- 5. Store it on the server Send the subscription to your API, which keeps it for that person - in {{brand}}, for that sign-in, so signing out stops the pushes.
- 1. Build a small payload JSON with a title, a body, the link to open and a tag. Keep it small - a push holds about 4 KB.
- 2. Encrypt it for that browser Only the browser's private key can read it (RFC 8291); the push service carries it blind.
- 3. Sign a VAPID token A short JWT naming the push service (aud), an expiry (exp) and your contact (sub), signed with your private key (RFC 8292).
- 4. POST it to the endpoint With the headers TTL (how long to keep trying), Urgency, Content-Encoding: aes128gcm and Authorization: vapid t=..., k=....
- 5. Read the answer 201 accepted. 404 or 410: the subscription is gone, delete it. 429 or 5xx: keep the subscription, wait (honour the Retry-After header if present) and retry with growing delays.
- 6. The browser shows it The service worker's push event calls showNotification. A tap fires notificationclick, which focuses an open window or opens one at the link.
iPhone and iPad On Apple's phones and tablets a web app can receive pushes only after it is added to the home screen (iOS 16.4 and later). Detect this and explain it, instead of showing a button that cannot work.
From one event to many people: fan-out
When a card is assigned or a check starts failing, several people may need to know. Fan-out is the step that turns one event into one delivery per person, on each channel they want. {{brand}} uses fan-out on write: at the moment of the event, one inbox row is written for each recipient.
- 1. Decide who The module names the recipients by id - the assignee, the watchers, the reviewers.
- 2. Group For each person, look for an unread row with the same group key within a recent time window. Found: add one to its count - 3 new comments on Fix the login redirect. Not found: write a new row.
- 3. Save with the change The rows can be saved in the same transaction as the change they describe, so both are kept or lost together.
- 4. Hand over for devices Only events that ask someone to act now are handed to a background queue, so the request never waits for a push service.
- 5. Fan out to devices A background worker keeps people who chose instant delivery, writes the sentence once per language and posts to each of their devices.
- 6. Fan in at read time The inbox merges every module's rows into one list: unread first, Important before Normal before Routine, newest first.
- Fan-out on write Write one row per recipient when the event happens.; Opening an inbox is one cheap query; Each person's read state and count live on their own row; Easy to find and erase one person's rows; A team of 500 means 500 rows for one event; Wrong for audiences of millions
- Fan-out on read Store the event once and build each person's list when they open it.; One write per event; Fits huge audiences; Every read joins events, memberships and read markers; Unread counts become expensive
- Hybrid Write for most people, read for very large audiences.; Scales like a social network; Two code paths to keep correct
The options in the market
- In the database A binary column holds the bytes.; One backup and one transaction for row and file; Fine for a few tiny files; The database grows fast and backups slow down; Every download goes through the database
- Local disk only A folder on the server.; Nothing to set up; Fast; Lost with the server; Two servers do not share it
- One vendor SDK in every feature Features call Azure or AWS directly.; Every vendor feature is at hand; Every feature is tied to one vendor; Tests need the cloud or an emulator
- One interface with adapters Local disk, Azure Blob Storage and any S3-compatible store behind one contract.; Features never change when storage does; Disk on a laptop, cloud in production, MinIO on a customer's own servers; Tests run with no cloud account; Only what every backend can do reaches features; One more layer to keep
- Your own mail server Run Postfix and deliver directly.; Full control; A new server's IP has no reputation; You handle bounces, blocklists and DKIM keys yourself
- SMTP relay through a library MailKit hands each message to a relay over SMTP.; Works with any provider or a company's own relay; Fits a network that cannot reach a vendor's API; Fewer delivery details come back; Connections and TLS are yours to configure
- HTTPS email API Azure Communication Services, Amazon SES, SendGrid, Postmark.; The provider handles reputation, DKIM and bounces; One HTTPS call; can sign in with a cloud identity instead of a password; A vendor-specific API; Sending quotas per minute and hour
- Standard Web Push with VAPID Post encrypted messages to each browser's push service.; Chrome, Edge, Firefox and Safari with no SDK and no vendor account; The payload is end-to-end encrypted; On iPhone only for web apps on the home screen; Small payload; delivery is best effort
- Firebase Cloud Messaging SDK Google's messaging SDK in the web app.; Topics, analytics, also native apps; A Google project and SDK in the product; Another company sees who gets what, when
- A push vendor A hosted notification service with dashboards.; Segments and dashboards out of the box; The recipients' devices live in someone else's system; Cost grows with the audience
- Native mobile apps APNs for iOS, FCM for Android.; The most reliable delivery and the richest notifications; Two more apps to build and get through store review
What {{brand}} chose, and why
The same product runs as a cloud service, on a single machine and inside a customer's own network. That one fact drives every choice here: each outside system sits behind an interface, a configuration value picks the adapter, and a deployment with a half-set provider refuses to start rather than failing someone's upload an hour later.
- Files: one contract with three adapters - local disk, Azure Blob Storage and any S3-compatible store (AWS S3, MinIO, Ceph). An organization can also connect its own Azure Blob or S3 storage; its records' files are then saved there, and downloads come as short-lived signed links straight from that storage.
- Scanning: every upload passes one scanning step wrapped around the store, which streams it to ClamAV, so a new feature cannot forget it.
- Email: an email API for the hosted service and SMTP through a mail library for an operator's own relay - both are used, depending on the edition - plus a development sender that saves emails locally.
- Push: standard Web Push with VAPID, no push SDK and no push vendor account.
- Notifications: fan-out on write into each module's own table, grouping by key, three choices per module (Instant, Digest, Muted), and people stored only as ids.
The trade-offs accepted Features get only what every backend can do, so no image resizing or lifecycle rules inside the storage. Web Push is best effort: a phone that stays off past the TTL misses the message, which is why the inbox row - not the push - is the record. Fan-out on write costs one row per person per event; grouping keeps that small.
Another choice would be right elsewhere. A social product whose accounts have millions of followers needs fan-out on read for its biggest audiences. A mobile-first product would build native apps. A product that only ever runs on one cloud could use that cloud's storage features directly and skip the interface.
Signed links: let the storage serve the file
Normally the API streams every download itself. When the file sits in an organization's own storage, streaming it through the API would double the traffic and the bill. Instead the API checks access as usual and answers with a signed URL: the storage itself checks the signature and the expiry, then serves the bytes.
- 1. Ask The browser asks the API for a file and says it can follow a link.
- 2. Check The API checks the record's permissions exactly as for any download.
- 3. Sign The adapter signs a URL for one object, read only, expiring in a few minutes, that forces Content-Disposition: attachment with the real file name.
- 4. Follow The browser opens the link as a normal page visit and the storage serves the file.
- 5. Expire After the expiry the link is worthless, so a link copied into a chat stops working quickly.
A signed link is a bearer credential Whoever holds the link can download the file until it expires - no sign-in needed. Keep lifetimes short, sign only after the permission check, never log the full URL and never put one in an email that will sit in an inbox for weeks.
Trust the bytes, not the label
The browser sends a Content-Type with each upload, but the uploader controls it. A file named cat.png can be an HTML page with a script in it; served as a page from your domain, that script runs as your site. Most file formats begin with fixed magic bytes, so the server can check what a file really is.
- 1. Peek Read the first bytes and rewind, so the whole file is still there to store.
- 2. Proven If the bytes prove an image or a PDF, store and serve it as that type.
- 3. Unproven bytes If the bytes prove nothing, treat the claimed type as a hint only. Serve it as that type only if it can never run in a browser; otherwise serve a download.
- 4. Everything else Anything that can run in a page - HTML, SVG, XML, scripts - is only ever served as a download, never rendered.
Virus scanning with ClamAV
ClamAV is a free, open-source antivirus engine. Its daemon clamd loads the signature database once (the companion tool freshclam keeps it up to date) and listens on TCP port 3310 or a Unix socket. An application streams the file to it with the INSTREAM command and reads a one-line verdict - no file has to be written anywhere first.
- 1. Connect Open a TCP connection to clamd.
- 2. Say what you want Send zINSTREAM followed by a zero byte. The z means commands and replies end with a zero byte.
- 3. Send chunks Each chunk is a 4-byte big-endian length followed by that many bytes of the file.
- 4. Say you are done Send a chunk of length zero.
- 5. Read the verdict stream: OK is clean; stream: <threat> FOUND is infected; anything ending in ERROR - for example a stream larger than clamd's StreamMaxLength - is no verdict at all.
- Before saving, inline The upload waits for the verdict.; An infected file never reaches storage; The uploader hears at once and why; Uploads take a little longer; The scanner must be running
- After saving, with quarantine Save first, mark the file pending, scan in the background.; Fast uploads; Files can be scanned again with newer signatures; A window in which a bad file exists; Every download must respect the pending state
- The cloud's malware scanning The storage service scans for you, for example Microsoft Defender for Storage.; Nothing to run; Tied to one cloud; Results arrive after the upload
Test with EICAR, never with real malware The EICAR test file is a harmless 68-byte text that every antivirus engine reports as infected. Download it from eicar.org to test your upload path; an error reply must always count as no answer, never as clean.
Deliverability: SPF, DKIM and DMARC
Anyone can write any From address into an email. Receivers decide whether to believe it with three DNS records on the sending domain. Without them, confirmation and password-reset emails land in spam or are refused.
- 1. SPF Is the sending server's IP allowed by the SPF record of the envelope sender's domain?
- 2. DKIM Does the DKIM-Signature header verify with the key in DNS, proving the body and key headers were not changed?
- 3. Alignment Does the domain that passed SPF or DKIM match the From domain the reader sees?
- 4. DMARC If neither aligned check passes, apply the domain's policy and send an aggregate report to the address in rua.
- Start DMARC at p=none, read the reports for a few weeks, then move to quarantine and finally reject.
- DKIM survives forwarding, SPF does not - DKIM is what keeps a forwarded message passing DMARC.
- Since 2024 Gmail and Yahoo require bulk senders to have SPF, DKIM and DMARC, a one-click unsubscribe, unsubscribes honoured within two days, and a spam-complaint rate kept well under 0.3%.
- Send recurring email only to confirmed addresses: fewer bounces, better reputation.
One-click unsubscribe, done right
A recurring email - a weekly summary, a digest, tips for the first week - must be easy to stop. The List-Unsubscribe header (RFC 2369) gives mail clients their own Unsubscribe button; List-Unsubscribe-Post (RFC 8058) makes it one click: the client sends a POST to the address and the email stops, with no page and no sign-in.
- 1. A personal token Each recurring email carries a personal token that identifies the recipient and the kind of email, can be checked without trusting the client, and stops only that one email.
- 2. One-click POST A mail client's button posts to the address and the email stops at once.
- 3. A link in the body A person clicking the link in the email gets a page that shows the subscription and changes nothing until they press a button.
- 4. A wrong token A token that does not check out is rejected with a plain not found answer, revealing nothing about why.
A GET must never unsubscribe Company mail scanners open every link in incoming email to check it. If opening the link stopped the email, scanners would unsubscribe people who never asked. Only a POST changes anything.
Priority lanes and pacing for outgoing mail
Email providers allow only so many messages per minute and per hour. A good design keeps two lanes: urgent mail such as a password reset goes out at once, and bulk mail such as invitations takes its turn within a share of the quota, so a burst of bulk mail can never delay an urgent message.
- 1. Take a turn Count one more in this minute's and this hour's counter. Over the share: queue it.
- 2. Send A send that fails is queued for another try instead of failing the work that sent it.
- 3. Wait safely A queued message holds links and tokens, so it is stored protected, never as readable text.
- 4. Retry A background worker sends the queue in order, within the same share, with growing waits between tries.
- 5. Finish Sent: the stored copy is removed. Too many failures, or too old: dropped, because its links would be stale.
The counters must live in a store that every server shares, such as a cache server. Otherwise each server would think it had the whole share to itself. A fixed window (this minute) is simple, but a burst at the edge of two minutes can double up; a sliding window is smoother but costs more to keep.
Inside a push message: encryption and VAPID
A push travels through a company you do not control, so the payload is encrypted end to end for one browser, and the request carries a VAPID signature so the push service knows which server sends it. Both are short standards worth knowing for interviews.
- 1. A fresh key pair The server makes a new P-256 key pair for this one message.
- 2. A shared secret ECDH (elliptic-curve Diffie-Hellman) lets two parties that never met agree on a secret by exchanging only public keys. The server's fresh private key and the browser's p256dh public key give a secret only those two can compute.
- 3. Mix in the auth secret HKDF-SHA256, a function that stretches a secret into proper key material, combines the shared secret with the browser's 16-byte auth secret and both public keys. The browser created the auth secret for this subscription, so only a sender who was given the subscription can make a message the browser accepts.
- 4. Key and nonce HKDF again, with a random 16-byte salt, gives a 16-byte AES key and a 12-byte nonce. A fresh salt makes every message use a different key and nonce, which AES-GCM requires: never the same nonce twice with one key.
- 5. Encrypt AES-128-GCM encrypts the JSON plus one padding byte as a single record of at most 4096 bytes.
- 6. The body Salt, record size, the server's one-time public key, then the ciphertext - sent with Content-Encoding: aes128gcm.
The subscription is untrusted input The endpoint URL comes from the browser. Accept only https addresses of real push services, do not follow redirects, and treat the endpoint and auth secret as a credential: never log them, and delete them on 404, 410 and sign-out.
Taming volume: grouping, priorities and digests
More notifications do not mean better ones. Without grouping and batching, a busy team trains people to block notifications, and a blocked permission is rarely given back. Three techniques keep the volume humane: grouping folds repeats into one row with a count, priorities put what needs action first, and preferences let each person choose per module how much reaches them.
- Important: asks the reader to act - an assignment, a mention, a review, a failure on production.
- Normal: news about the reader's own work - a comment on a watched card.
- Routine: background a busy day produces - a card moved, a check passing again.
- 1. Find candidates On a regular sweep, every half hour or so, find people with unread messages older than a couple of hours and no digest in the last half day.
- 2. Skip the wrong moments Skip anyone online right now, anyone who turned the module off and anyone who stopped the email.
- 3. Respect the choice A person who chose batched delivery gets at most one a day.
- 4. Write it for the reader In the reader's language, listing each conversation once, with a one-click unsubscribe.
- 5. Remember Mark what the digest covered, so the next one starts after it and a retry never sends the same items twice.
Storing people as ids, not names
A notification sentence such as Aynur assigned you Fix the login redirect is not stored as words. The row keeps a template key, the values that fill it and the ids of the people it names. The sentence is written when it is read, in the reader's language, with names looked up at that moment.
- Renames A person who changes their name reads by the new name everywhere, at once - no row is rewritten.
- Languages Russian and Azerbaijani readers each get the sentence in their language from the same row.
- Erasure When a person is erased, their id in other people's rows becomes a fixed placeholder id that reads as Deleted user. No sentence holds their name, so none needs rewriting.
- Small pushes A push carries the sentence, a link, a tag and at most one next-step button - the record itself is fetched after the tap, behind sign-in.
- Store as little personal data as the job needs: keep a stop list as an irreversible fingerprint instead of the address, protect anything that waits in a queue, and log that something happened, not what it said.
- Tie a push subscription to one sign-in, so signing out removes it and a shared computer never shows the last person's notifications.
Try it yourself: Web Push, a virus scan, email and signed links
All the exercises use free tools only. Service workers need https or localhost, so serve the page with a local web server on localhost.
- 1. Make VAPID keys npx web-push generate-vapid-keys prints a public and a private key.
- 2. A page and a service worker Save the two snippets from the Web Push section as index.html (inside a script tag, wrapped in a function that runs when a Subscribe button is pressed) and sw.js. Write the small base64UrlToBytes helper yourself, paste your VAPID public key into the page, make the page print JSON.stringify(subscription), and serve the folder with npx serve . (open http://localhost:3000). Check Application > Service Workers in DevTools.
- 3. Send Run the script below with the printed subscription: the notification appears even with the tab in the background.
- 4. Tap Handle notificationclick to open your page at the message's link.
- 5. Break it Unsubscribe in the browser and send again: the push service answers 410, and your code should delete the subscription.
- 1. Start clamd Run the official clamav/clamav image; the first start downloads the signature database, which takes a few minutes.
- 2. Ping it Send nPING and expect PONG - the n prefix means newline-terminated.
- 3. Scan the test file Download the EICAR test file from eicar.org and scan it: clamd names the test signature.
- 4. Write your own client Send the same file with INSTREAM from a short program, using the byte layout above.
- 1. Start a mail catcher Run Mailpit, a free fake mail server: it accepts any message on port 1025 and shows it in a web page on port 8025.
- 2. Send a message With the nodemailer package, send one message to localhost:1025 with a text part, an HTML part and the List-Unsubscribe and List-Unsubscribe-Post headers.
- 3. Read the source Open http://localhost:8025 and use View source to find the multipart/alternative boundary and the headers you set.
- 4. Sign a link Run MinIO, a free S3-compatible store, create a presigned GET URL with the AWS SDK, open it, then open it again after the expiry and see the failure.
Common mistakes
- Reading uploads into memory: a byte array per upload falls over under load. Stream, set a body limit per endpoint, and let large form parts spool to disk.
- Trusting the file name or the claimed type: store under random keys, check the magic bytes, and serve HTML, SVG and XML only as downloads.
- Unsubscribing on GET: mail scanners open every link. Show a page on GET and change things only on POST.
- Pushing everything: people who get a push for every routine event block notifications for good. Push only what asks someone to act; group the rest in the inbox.
- Keeping dead subscriptions: delete a subscription on 404 or 410 and when the person signs out, or a shared computer keeps showing someone else's notifications.
- Writing names into notification text: renames show stale names and erasure means rewriting other people's rows. Store ids and render at read time.
Interview questions
- A user uploads cat.png that is really an HTML page. What do you do? Never trust the name or the claimed type. Read the first bytes, store under a random key, serve only proven image types inline and everything else as an attachment with nosniff, and scan the stream before it is saved.
- How do you accept a 2 GB upload without running out of memory? Stream it: let the form reader spool large parts to disk, pass the stream to the scanner and the storage adapter, and never copy it into a byte array. Set a body limit per endpoint and refuse a larger declared length with 413 before reading.
- What is a signed URL and when do you use one? A URL carrying a signature, permissions and an expiry, so storage serves one object without your API in the path. Use it after your own permission check, with a short lifetime and a forced attachment disposition.
- How would you add virus scanning to uploads, and what if the scanner is down? Wrap the file store so every upload streams through ClamAV's INSTREAM before it is saved, and refuse infected files. When the scanner cannot answer, fail closed where rules demand it or fail open with a warning - and never treat an error reply as clean.
- What do SPF, DKIM and DMARC each do? SPF lists the servers allowed to send for a domain, DKIM signs each message with a key published in DNS, and DMARC tells receivers what to do when neither aligns with the From domain. Together they stop spoofing and keep real mail out of spam.
- What does List-Unsubscribe-Post do, and why must a GET not unsubscribe? It tells mail clients they may POST to the List-Unsubscribe address for a one-click stop (RFC 8058). Security scanners follow every link with GET, so a GET that unsubscribed would stop emails nobody asked to stop.
- Walk me through Web Push. The page registers a service worker and subscribes with the server's VAPID public key; the browser returns an endpoint and two keys, which the server stores. To send, the server encrypts the payload for that browser, signs a VAPID JWT and posts to the endpoint; the push service wakes the service worker, which shows the notification.
- What is VAPID for, and how is the payload protected? VAPID is an ES256-signed JWT that identifies the sending server to the push service, and a subscription only accepts pushes signed with the key it was made with. The payload is encrypted with ECDH, HKDF and AES-128-GCM for one browser, so the push service carries it unread.
- Fan-out on write or on read? On write, each recipient gets a row when the event happens, so reads and unread counts are cheap; on read, the event is stored once and each feed is built when opened. Workplace tools with teams of tens or hundreds suit fan-out on write; very large audiences need read or hybrid. For a workplace tool, choose fan-out on write and use a hybrid only for the few very large audiences.
- Why store people as ids in notifications? Names change and people can be erased. With ids the sentence is rendered at read time in the reader's language with current names, and erasure swaps an id for a placeholder instead of rewriting text in other people's rows.
Related
- /help/technology/realtime-events
- /help/technology/secure-links-privacy
- /help/technology/private-edition