Skip to content
Buse Turgay Sönmez

Design Engineer, Belgrade

Fugax

Fugax shares a snippet, a note or a file once and then forgets it. The text is encrypted in the browser with AES-256-GCM and the key rides in the URL fragment, which a browser never sends to a server. The server stores bytes it cannot read and deletes them on a timer or after a set number of reads.

  • 2026
  • Encryption
  • CLI
  • Next.js
  • Tests

In short

Status
Built through all 11 planned phases. Not deployed yet, and not independently audited
My part
Product, interface, architecture and review, with contributions from one other developer. Built with AI agents
Encryption
AES-256-GCM, keys derived with HKDF-SHA256, optional passphrase through Argon2id
Clients
Web, CLI, VS Code extension, browser extension, embed script: one core package
Secret scanner
13 patterns in the browser, with checksum checks for Turkish ID numbers and IBANs
Stack
Next.js 16, React 19, TypeScript, Drizzle, MySQL, Vitest
Tests
22 test files, including 34 integration cases against a running server and a real database

The key never reaches the server

A share link looks like an address with something after a hash sign. That last part is the key, 32 random bytes, and browsers do not send it: not in the request and not in the Referer header. The server receives the ciphertext, the nonce, the salt and the policy, and nothing that opens them. Link and key can also be sent separately, over two channels.

The policy, meaning burn after reading and the expiry time, goes into the encryption as authenticated data. If a server changed either, decryption would fail instead of quietly obeying. Attachments get their own derived keys, and the number of chunks is authenticated too, so a truncated file is caught.

A passphrase, when there is one, goes through Argon2id and is combined with the link key rather than replacing it. The Argon2 code loads only when a passphrase is used.

Burning, without races and without bots

Reading a share is a POST, not a GET. Chat apps fetch every link to draw a preview, and a GET would let that preview burn a one-time message before its reader arrived. Opening also takes a row lock, so two requests landing together cannot both pass a limit of one read.

A share that is gone answers 410 and one that never existed answers 404, because those are different facts. A purge job then removes expired rows, their files on disk, uploads that were announced and never finished, and folders left behind by either.

One envelope, five clients

The web app, the command line tool, the VS Code extension, the browser extension and the embed script all seal and open the same envelope, because all of them import the same core package, and a cross-client test keeps it that way. Three copies of the crypto would have drifted apart one day.

Before anything is sent, a scanner in the browser looks for 13 kinds of secret, from cloud keys and tokens to private keys and connection strings. Turkish ID numbers and IBANs are checked against their checksums, so a random 11 digit number does not raise an alarm. It warns and suggests a shorter life for the share. It does not block.

What the database does not hold

No plain text, no file names, no language, no title, no raw IP address, no user agent, no referrer. Rate limiting uses a keyed hash of the address with a salt that rotates daily. The delete token stays in the browser that created the share, and the server keeps only its hash.

Controls

Speed
1.00×
Reads allowed
1
--ground
oklch(0.985 0.003 85)
--surface
oklch(0.958 0.005 85)
--raised
oklch(1 0 0)
--ink
oklch(0.2 0.006 85)
--muted
oklch(0.43 0.008 85)
--faint
oklch(0.5 0.008 85)
--line
oklch(0.9 0.004 85)
--line-strong
oklch(0.6 0.006 85)
--accent
oklch(0.66 0.22 38)
--accent-ink
oklch(0.52 0.19 38)
--stage
oklch(0.152 0.004 85)
--radius-stage
36px
--radius-stage-sm
28px
--radius-media
24px
--radius-panel
16px
--radius-field
10px
--ease-out-expo
cubic-bezier(0.16, 1, 0.3, 1)

This week · W39One WebGL context, three leaksGeometry · 2026