Envoy
Every developer knows the friction: you're setting up a new machine or switching between your laptop and desktop, and you need your environment variables. The .env file sitting in your project's .gitignore isn't going anywhere useful. So you do what everyone does—copy it to a notes app, message it to yourself, or dig through old Slack DMs hoping you saved it somewhere. It works, but it's the kind of solution that feels wrong every time you do it.
I kept running into this problem and decided to build something proper. The result is Envoy—a Git-like CLI for syncing encrypted environment variables across machines and teams.
The Problem Space
Environment variables are weird. They're essential to every project but exist in this uncomfortable space where they can't be version controlled (for good reason) but also aren't treated as first-class artifacts worth managing properly. Most solutions either require trusting a third-party service with your plaintext secrets or involve some combination of encrypted files and manual key distribution that's tedious to maintain.
I wanted something that felt native to a Git-based workflow, where syncing secrets would be as natural as pushing and pulling code—but with a security model that assumed the server couldn't be trusted.
Architecture & Security Model
The core design principle is zero-trust for the server. Every cryptographic operation happens client-side before anything touches the network:
- Key derivation via Argon2id (memory-hard, resistant to GPU attacks)
- Payload encryption with XChaCha20-Poly1305 (authenticated encryption with extended nonces)
- Content addressing and integrity verification via SHA-256
The server only ever stores and serves ciphertext. Even a complete database breach exposes nothing usable without the user's passphrase. This also means the server implementation can be relatively simple—it's essentially a dumb blob store with authentication, which reduces the attack surface significantly.
For authentication, I implemented GitHub OAuth using the device flow. This avoids the need for a separate account system and works well for a CLI context where you can't easily redirect to a browser callback URL.
Building the CLI in Rust
I chose Rust for the CLI primarily because I wanted to learn it properly, but it turned out to be a great fit for the project. The strong type system caught several logic errors at compile time that would have been subtle runtime bugs in other languages, particularly around the encryption and file handling code.
Working with Rust's ownership model took some adjustment, especially when dealing with file I/O and passing encrypted data between functions. The cryptography ecosystem is mature—I used argon2 for key derivation and chacha20poly1305 for encryption, both well-audited crates that follow best practices.
One of the more interesting challenges was implementing a Git-compatible object model from scratch to manage local state. Instead of a simple database, I designed a file structure that mimics Git internals: .envoy/HEAD tracks the current commit, .envoy/refs monitors remote branches, and .envoy/cache stores content-addressed encrypted blobs.
Cross-compilation was another learning curve. Getting consistent builds across macOS (both Intel and Apple Silicon), Linux, and Windows required understanding cargo's target system and setting up GitHub Actions matrix builds. The release pipeline now automatically compiles binaries for all platforms and publishes them with each tagged release.
The Backend
The API is built with Hono, a lightweight TypeScript framework that's become my go-to for serverless APIs. It runs on Vercel, with the Next.js wrapper primarily there to facilitate hosting and set up for a future web dashboard—currently the frontend is just a static landing page.
Prisma handles the database layer, which made schema iteration fast during development. The data model is straightforward: users (linked to GitHub accounts), projects, and encrypted blobs with their associated metadata. Since the server never handles plaintext, the API routes are simple CRUD operations with auth checks.
Choosing this stack let me move quickly while keeping the door open for a proper web interface later. The Hono API is clean enough that it could be extracted to a different runtime if needed.
The Storage
The blobs are all stored on a Cloudflare R2 bucket. I chose Cloudflare for it's ease of setup and being free for the scale I'm running this app on.
Usage
envy init # Initialize project & .envoy structure
envy encrypt # Encrypt .env files (tracks intent)
envy commit -m "message" # Create a versioned, encrypted commit object
envy push # Upload blobs and update remote HEAD
envy pull # Download and decrypt to original paths
envy status # Check sync state (offline-safe)
What I Learned
This project pushed me across the full stack in ways that felt meaningful. On the Rust side, I got comfortable with the ownership model, error handling patterns with Result types, and structuring a CLI application with clap. The cryptographic work forced me to actually understand the primitives I was using rather than just calling library functions.
On the infrastructure side, setting up CI/CD for multi-platform Rust compilation was valuable. Matrix builds, release automation, and install scripts for different operating systems are the kind of DevOps knowledge that transfers to any project.
Perhaps most importantly, building something I actually use daily meant I couldn't cut corners on UX. Error messages needed to be helpful, commands needed to behave predictably, and the mental model needed to be intuitive for anyone familiar with Git. Dogfooding your own tools is a fast way to find the rough edges.