advanced one sitting social-media
Build your own ephemeral snap relay (a personal Snapchat)
You will build a private snap relay: a camera-first web app your friends install to their home screen, where photos delete after one timed view, group stories vanish after 24 hours, and streaks count daily exchanges. The mechanics are a small server plus a delete timer. Snapchat survives untouched because the app is the friend graph: you can clone every feature and still clone none of the value, since the people, the AR lens models, and the global media pipeline stay on their side.
What you'll learn
- Capturing photos in the browser with getUserMedia and compositing caption overlays on a canvas
- Building an installable PWA with a service worker that caches the shell but never the private media
- Implementing view-once delivery where the server deletes the file the moment it is viewed
- Web push notifications with VAPID keys, including graceful behavior when permission is denied
- Testing time-dependent logic (streak day boundaries, 24-hour sweeps) with a fake clock
Before you start
- Node 22 installed locally
- A small VPS or free-tier Node host you can put behind HTTPS (camera and push APIs require it)
- A domain name you control, for DNS and the Caddy certificate
- A friend group willing to install the PWA and actually use it
The build
Start with the skeleton: a Fastify server, a SQLite database, and an installable PWA shell served without a build step. Accounts are invite-code only, seeded by a CLI script, because a private network's first security layer is that strangers cannot sign up at all. HTTPS matters from the first minute: browsers refuse camera and push access over plain http, so local dev runs on a self-signed certificate.
step prompt
Build the project scaffold for a private snap relay. Requirements: - Node 22 + Fastify + better-sqlite3 with WAL mode, no build step, static files served from /public. - Vanilla-JS installable PWA: manifest.json plus a service worker that caches the UI shell for offline but never caches snap images. - Invite-code accounts: scripts/seed-invites.js writes codes into SQLite, signup is username plus passcode, sessions are httpOnly cookies. - Media lives as files in ./data/media outside the web root; metadata in ./data/app.db. - SESSION_SECRET in .env, with .env.example and a scripts/gen-secrets.js that generates values. - Local dev over HTTPS with a self-signed cert; document the browser warning and the trust step in README. - Out of scope: styling polish, third-party auth, any test runner beyond node:test. - Verify: npm start serves https://localhost:8443 and signup with a seeded code creates a row in the users table.
Now the core loop's front half: open the camera, capture a still, add a caption, send it. getUserMedia is the browser API that grants camera access, and its behavior differs across phones, browsers, and permission states, so test on a real device from the start. The caption is drawn on a canvas layered over the photo and flattened to a single JPEG on send.
step prompt
Add the snap capture and send flow. Requirements: - Open the camera with getUserMedia, front or rear, and capture a still to a canvas. - Caption layer: type text or draw with a finger on the same canvas, flattened to one JPEG on send. - POST the JPEG to /api/snaps with a recipient list; the server writes the file to ./data/media and one row per recipient in a snaps table. - Recipient picker lists users created via the step 1 invite flow. - Export JPEG at quality 0.8 to keep uploads near 2 MB, phones on weak connections stall on full-resolution images. - Out of scope: video capture, filters of any kind. - Verify: send a snap to yourself from a phone on your network and watch the file appear in ./data/media.
This is the promise the whole app makes: one view, up to 10 seconds, then the file is gone from the server. Deletion has to happen server-side at view time, not client-side, or it is theater. Streaks are the retention mechanic: a counter per friend pair that increments once per calendar day both directions have sent, which makes day-boundary math the classic hidden bug.
step prompt
Add view-once delivery and per-pair streaks. Requirements: - GET /api/snaps/:id streams the image once, marks the row opened, then deletes the file immediately; only sent and opened timestamps remain. - Client shows the snap full-screen for up to 10 seconds then returns to the inbox; reopening shows 'deleted from our server'. - Streaks table keyed on the sorted user pair, incremented once per calendar day when both directions have sent. - Day-boundary math uses the server clock in one documented timezone, mixing client and server clocks here causes phantom streak breaks. - Tests with node:test and a fake clock: delete-after-view removes the file, and a streak survives a send at 23:59 answered at 00:01. - Honest UI copy: state plainly that screenshots cannot be detected on the web. - Out of scope: read receipts beyond the opened flag, resend or retry queues.
Stories are snaps posted to the whole group, hard-deleted after 24 hours by a background sweep. A sweep job is a scheduled task that scans for expired rows and removes both the database row and the file on disk. Time-based deletion fails silently when it fails, so the sweep logs what it deleted and the tests drive it with the fake clock from step 3.
step prompt
Add 24-hour group stories. Requirements: - POST /api/story adds a snap visible to all members; story rows live in a stories table pointing at files in ./data/media. - A sweep job runs every 10 minutes and hard-deletes story files and rows older than 24 hours. - Story viewer shows items oldest first with tap-to-advance. - Sweep test using the step 3 fake clock: an item aged 23h50m survives, one aged 24h10m is gone, both file and row. - Log each sweep run with a deleted-item count so you can verify it on the server later. - Out of scope: per-viewer watch receipts, story replies.
A messenger nobody is notified by is a mailbox nobody checks. Web push uses VAPID keys, a public/private key pair that identifies your server to browser push services. Ask for notification permission only after the learner has sent a first snap; permission prompts on first load get reflex-denied. The app must work fully without push, because on iOS it is best-effort by platform design.
step prompt
Add web push notifications for new snaps and stories. Requirements: - Use the web-push library with VAPID keys generated by scripts/gen-secrets.js from step 1 and stored in .env. - Request permission only after the user sends their first snap, never on first load; store subscriptions in a push_subs table. - Notify recipients on new snaps and story posts; a failed push must never block or delay the send itself. - Full functionality with push denied; README states iOS web push is best-effort. - Test: a subscription returning HTTP 410 is pruned from push_subs. - Out of scope: notification preference UI, unread badge counts.
Rent a small VPS, point your domain's DNS A record at it, and put Caddy in front of the Node app so certificates renew themselves. Copy .env up, run the invite seeder from step 1, and hand codes to your friends in person. Walk each one through installing the PWA from the browser share menu, since that step loses people. Then use it for a week and notice who actually opens it: that observation is the network-effects lesson arriving firsthand.
What you won't get
- Your snap partners must opt in and install your app; everyone you currently snap with stays on Snapchat's graph
- AR lenses and face filters, which rest on years of proprietary face-tracking models
- Screenshot detection: browsers have no API to see or stop a screenshot, and your UI should say so plainly
- Snap Map, Discover, Spotlight, and the creator ecosystem around them
- Messenger-grade mobile push: web push on iOS is best-effort and will occasionally arrive late or not at all
Why people still pay — and what that teaches you
network-effects: The graph is the product. A disappearing photo is only valuable because of who opens it, so a feature-complete clone with two users is a group chat with extra steps. The builder's lesson: audit any product for whether the value lives in the software or in who else is using it before you rebuild it.
proprietary-models: Lenses look like a filter feature but are frontier computer-vision work: years of face-tracking models, training data, and on-device optimization. When a 'feature' is really a research program, a solo builder should scope it out explicitly rather than budget a weekend for it.
scale-infra: Snaps ride a global media pipeline moving billions of images a day with low latency. Your single VPS handles one friend group fine, and that contrast teaches the real cost curve: features are cheap to copy, operating them for everyone at once is the expensive part.
Stretch goals
- Add finger-drawing with color selection to the caption canvas
- Study Signal's view-once media design and add client-side encryption so the server never holds readable images
- Read Pixelfed's federated stories model and consider whether federation could ever solve your two-user network problem
All steps done — did it work?
Congratulations. Tell someone what you built.
About Snapchat
Snapchat costs $3.99/month. Almost nobody pays for Snapchat itself · the app is free and the subscription sells cosmetics and vanity telemetry: pin a #1 BFF, see who rewatched your story, wear a badge. People pay 3.99 USD because the network underneath is where their friends live, and status inside a graph only matters in a graph with people in it. That is also exactly why a rebuild is pointless: you can clone every feature and you will have cloned none of the value.
Sources & further reading
- Signal for Android (source) — A real open-source implementation of disappearing messages and view-once media to compare your deletion design against
- Pixelfed (source) — Federated photo sharing with 24-hour ephemeral stories, the closest open ecosystem to the story half of this build
- Snapchat+ plan page — Worth a look at what the paid tier actually sells: cosmetics and status inside the graph, not features
Keep building
New lessons and honest build notes, by email. No spam, one-click out.
Signups open when the site goes live.