Building a Local-First Secret Generator: A React, TypeScript & Trust-Model Case Study

Demo:
ViewO1. The Challenge
Fellow developers, we all need strong secrets constantly: API keys, JWT salts, webhook tokens, Postgres passwords, the whole pile that eventually lands in .env. And the tools out there for this tend to pull in one of two directions.
Either they're throwaway generators with murky entropy and no real paste-into-project workflow, or they've rebranded themselves as "credential platforms" and quietly signed you up for sync theater you never asked for.
I wanted something that actually matched how I work day to day:
- Generate in the browser with real Web Crypto, not a black box
- Fill a single value, a batch, or a full multi-key pack without ceremony
- Stay honest about what actually reaches a server
- Ship as a real, usable product, not a polished concept mock
That's how KeyBrew came together: a local-first secret generator, and honestly, a chance for me to think hard about trust, monorepos, and what it actually takes to ship something real.


O2. The Solution
KeyBrew generates cryptographically strong secrets built for real project work. Client-side generation is the default, not an afterthought. Optional accounts sync your presets and preferences only, never your secrets or your history. There's no analytics running in the background either.
A monorepo from day one
Here's the thing: crypto logic duplicated between a UI and an API is crypto logic that eventually drifts. So the product is split to avoid exactly that:
apps/web— React + Vite generator UI (Tailwind v4, shadcn/ui)apps/api— Express API for health, config, profiles, auth, and preset syncpackages/shared— token profiles, the CSPRNG seam, presets, strength helpers, Zod options
The shared package is the source of truth. The web app generates locally, the API handles public profile metadata and optional sync, and a tooling-only generate route exists for scripts and tests, staying off in production unless you flip it on yourself.
Entropy that travels with the package
I built one small, well-tested seam around the browser's native crypto source, and that's the only thing in the whole codebase allowed to touch randomness directly. It runs the same way in the browser and in Node during tests, so there's never a gap between what gets tested and what actually ships.
I'm intentionally not walking through the internals here. The less detail floating around about exactly how a generator produces its randomness, the better, even when the approach itself is solid. What matters for this write-up is the design principle: isolate the one thing that has to be correct, test it hard, and let everything else in the app depend on it instead of reimplementing it.
That same profile logic in packages/shared drives what renders in apps/web and what the API validates, so client and server never disagree about what a "strong" secret actually looks like.
Presets, not just character soup
Here's where things get genuinely useful. KeyBrew ships opinionated presets for the secret shapes developers actually reach for:
- Hex secrets at common bit strengths
- Base64url tokens sized for JWT-style use
- UUID v4 identifiers
- Structured API-key formats with a prefix
- Passphrases built from a wordlist, not raw character noise
Each preset carries its own sensible default length and charset, so picking "JWT signing secret" gets you a sane starting point instead of a blank slider and a guess.
Strength you can see, not just claim
Rather than another fake "strength meter" powered by regex heuristics, KeyBrew computes real entropy from the actual generation parameters: charset size and length for character-based secrets, wordlist size and word count for passphrases. The UI shows the bit count and what that actually means in practice, not a colored bar pretending to understand your password strategy.
O3. Key Features
Multiple Generation Modes
- Single value generation for a quick secret
- Batch mode for generating many secrets at once
- Multi-key packs for scaffolding a full
.envin one pass
Trust-First Architecture
- Client-side generation by default; secrets don't have to touch a server
- Tab-scoped history, not a synced secret log
- Optional account sync covers structure (presets, preferences), never values
- No analytics, full stop
Shared Core
- One CSPRNG seam used in the browser and in tests
- One set of token profiles driving both UI and API validation
- Zod schemas keeping options honest end to end
Developer-Shaped Presets
- Common secret shapes ready to go: hex, base64url, UUID v4, prefixed API keys, wordlist passphrases
- Sensible defaults per preset instead of generic sliders
O4. Technical Highlights
Why client-side generation comes first
Web Crypto's getRandomValues is a CSPRNG sitting in every modern browser already. There's no good reason to round-trip a secret through a network request when the primitive lives right where the user is typing. Server-side generation becomes an option for specific flows, not the default posture.
Why a shared package beats duplicated logic
Token profiles, presets, and strength math all live in packages/shared, so apps/web and apps/api consume the exact same definitions. When a preset's default length changes, it changes once, everywhere.
Why sync stays narrow, on purpose
Stage 6 added accounts and sync, deliberately scoped to presets and preferences. Secrets and generation history are explicitly out of scope for sync. That's what keeps the honest-by-default promise intact, instead of KeyBrew quietly becoming "yet another vault" nobody asked for.
The Result
KeyBrew is:
- Live and usable for real
.envwork and day-to-day secret generation - Backed by a shared, tested CSPRNG and token-profile core
- Honest about its trust boundary: local generation by default, narrow optional sync
- Structured as a monorepo so the eventual Chrome extension (Stage 7) reuses
packages/sharedinstead of reinventing it
Lessons Learned
- Decide the trust model before writing the generator. Client-side-by-default shaped nearly every architectural decision that followed.
- Share the core, not just the types. Putting the CSPRNG and token profiles in one package kept the UI and API from quietly drifting apart.
- Presets beat sliders. Naming the shape ("JWT signing secret") is more useful than exposing every knob by default and hoping for the best.
- Real entropy beats a strength meter. Computing bits from charset and length is honest. A colored bar driven by regex isn't.
- Scope sync on purpose. Deciding what never leaves the browser matters just as much as deciding what does.
Conclusion
KeyBrew is a working answer to a small but constant developer annoyance: generating a secret that's actually strong, without pulling in a full vault product to do it. Built as a React and TypeScript monorepo with a shared entropy core, it's also a case study in scoping trust honestly, from client-side generation all the way to a sync layer that stays narrow by design.
The Chrome extension stage is next, and it'll lean on that same shared package, so the trust model doesn't change just because the surface does.
Technologies Used: React, TypeScript, Vite, Express, Zod, Web Crypto API, Tailwind CSS v4, shadcn/ui Key Skills Demonstrated: Monorepo Architecture, Cryptographic Design Decisions, Shared Package Strategy, Trust-Model Product Thinking, API Design