skip to content
2026#typescript#self-hosting#security

kUtils

Self-hosted encrypted notes, files, and shares - where security states are the interface, not the fine print.

The problem

Sharing a file or a note securely usually means trusting a service you cannot inspect, through an interface that hides the parts that matter: is this encrypted, who can see it, when does it expire, how many times can it be read? kUtils is a self-hostable answer where exactly those questions are the interface (not a settings page).

Architecture

kUtils is a Bun workspace shipped as one product: an Elysia API server, a React 19 web app, a CLI uploader, and a shared package holding the contracts and crypto both sides agree on. Share types cover encrypted files and notes, claim-based file requests (others upload to you, chunked and unauthenticated), short links, external redirects, and hosted setup packages. Lifecycle controls - expiration, finite read limits, burn-after-first-view, version history, trash - are explicit, visible properties of each share.

on the server

in the client

Argon2id

unwraps

wraps

AES-256-GCM, chunk index in AAD

password

password-derived key

random master key

per-item key

file or note

16 MiB chunks

encrypted chunks

wrapped key envelope

ciphertext

encrypted envelopes

Encryption happens in the client. The hierarchy is a password run through Argon2id (a memory-hard password derivation function) to unwrap a random master key, with per-item keys underneath; files use a chunked AES-256-GCM (authenticated encryption that detects tampering) format (16 MiB chunks, each independently authenticated, chunk index bound into the AAD (authenticated metadata that is not encrypted)) so a tampered or reordered chunk fails loudly. The server stores ciphertext, encrypted key envelopes (wrappers that protect cryptographic keys), and encrypted owner metadata. Even external redirect targets are encrypted client-side and decrypted from the URL fragment, so the server never learns where a redirect points.

The enforcement: a schema-wide inventory classifies every database column as client-encrypted, hashed, public, or unavoidable operational metadata - and tests fail when a new column lacks a classification. Where the server must read something (routing IDs, sizes, timestamps, rate-limit state), that is documented as the operational surface, not hidden.

kUtils keeps sessions in HttpOnly cookies with CSRF (checks that stop other sites from triggering actions as you) protection. Accounts can add TOTP with recovery codes, passkeys (device-bound credentials that replace passwords), trusted-device tracking, and step-up prompts for sensitive actions like key export - production refuses to start without the step-up policy enabled. The CLI mirrors the web features (passwords, expiry, burn-after-read, tags) with tokens shown once and stored only as hashes. The UI targets WCAG 2.2 AA: full keyboard operation, visible focus, accessible state on every security toggle, including the ones that explain their own trust cost.

Boundaries

The operator-blind design (a design that keeps user content encrypted from the host) has deliberate, labeled exceptions. Opting into server-side decrypted downloads sends the content key to the server - the UI says so in plain words. Owner-viewable share passwords move those passwords inside the operator boundary - also a labeled choice, hashed otherwise. The boundary statement is deliberately narrow: kUtils minimizes what the operator can read and tells you exactly where that minimum lies, rather than claiming an absolute it cannot keep.

It runs on your infrastructure - SQLite by default, Postgres/MySQL and S3 optional, systemd or Docker - and its update path stages, health-checks, and can roll back a release before production switches. The thesis it exists to test: security states you can actually operate beat security properties you have to take on faith (no thanks).