Back to Projects

Project

Ephemera - Zero-Knowledge Secret Sharing

A self-hosted, end-to-end encrypted app for sharing secrets that vanish. Text, links, and files are encrypted in the browser, expire by time or view count, and are erased from storage after they die.

Next.jsTypeScriptWebCryptotRPCPostgreSQLDockerCoolify

Overview

Ephemera is a self-hosted app for sharing secrets that vanish: passwords, tokens, config files, or anything else that shouldn't live in a chat history forever. Drops are encrypted in the browser with AES-256-GCM before upload, and the decryption key travels in the URL fragment, which browsers never send over the network. So the server stores ciphertext it cannot read.

It started as a much simpler expiring-links app, first deployed in January, and I rebuilt it from scratch in July 2026. The rebuild write-up covers the design decisions.

Features

  • Zero-knowledge encryption: content is sealed client-side. The key rides in the URL fragment, or it is derived from a passphrase with PBKDF2-SHA256 at 600k iterations.
  • Burns after reading: every drop expires by TTL, view count, or both. The default is a single view.
  • Reveal gate: opening a link never consumes a view, so Slack and iMessage link previews can't silently burn a one-time secret.
  • Actually ephemeral: a retention sweep blanks dead ciphertext and prunes view logs on a schedule.
  • Text, links, and files: text drops render markdown with syntax highlighting, URL drops get a cancelable redirect countdown, and files transfer encrypted.
  • Invite-only accounts: owner/admin/user roles, an admin panel, and per-drop analytics for views per minute, time to first view, and lifecycle timeline.
  • Privacy by default: viewer IPs are truncated before storage.

Technical highlights

Cryptography

The WebCrypto API does the encryption: AES-256-GCM envelopes with fresh IVs, random 256-bit keys carried in the #fragment, and a passphrase mode that stores only a salt. A wrong passphrase can retry against ciphertext already in memory. Typos never burn extra views.

Type-safe full stack

Next.js 16 App Router, tRPC 11 with the TanStack React Query integration, Zod 4 validation, and Drizzle ORM over PostgreSQL 16: one TypeScript type system spans from the database schema to the React components.

Tested guarantees

50 unit tests run the tRPC routers against PGlite (in-memory Postgres) using the real migration files. 6 Playwright flows prove the product promises in a real browser: the reveal gate consumes nothing, one-view drops burn exactly once, and users can't see each other's drops. CI runs lint, types, unit, e2e, and a Docker build on every push.

Zero-downtime schema migration

The live database predated migrations entirely (schema was pushed with drizzle-kit). The v2 migration runner detects that case, stamps a baseline as applied using drizzle-compatible bookkeeping, and then applies only the new migrations. The production upgrade was a single additive ALTER TABLE batch with no manual steps.

Deployment

Runs as a slim Next.js standalone image (Node 22) plus PostgreSQL on my Coolify instance, behind Traefik with automatic TLS. Migrations run at container start; health checks gate traffic.