We Built a Free SSL Tool and It Actually Works
How André and I built Certy, a free SSL certificate tool powered by Let's Encrypt, with a Rust backend, a SvelteKit frontend, and a Cloudflare Worker standing guard in the middle.
A few months ago, André Ribas and I sat down and asked ourselves a very reasonable question: why is getting a free SSL certificate still kind of annoying?
Let’s Encrypt exists. Certbot exists. The tooling is there. But if you’re not on a VPS with root access and a terminal open, the experience is… not great. You’re either wading through CLI flags, reading a wall of documentation, or hoping some hosting panel’s “auto-SSL” button actually works this time. It’s not that it’s impossible. It’s that it’s unnecessarily clunky for something that should just be a solved problem.
So we built Certy. It’s a web tool that lets you generate a free SSL certificate for any domain in a few minutes. No account. No credit card. No CLI required. Just a domain, an email, a DNS record, and you’re done.
The whole thing is open source at github.com/CertyBR, and I want to talk through how it’s built because honestly the architecture came out pretty nice.
The idea is simple. The execution is less simple.
On the surface, Certy does one thing: it talks to Let’s Encrypt on your behalf and hands you a certificate. You give us a domain and an email, you add a TXT record to your DNS, we verify it, and the certificate is yours.
The flow under the hood is more involved than that, but the user experience is the whole point. You visit the site, you fill in two fields, and you follow a few steps. No account creation. No waiting for a human to approve anything.
The certificates are valid for 90 days, which is standard for Let’s Encrypt. You get an email reminder before expiration. Renewal is just running through the flow again, which takes a few minutes.
The backend: Rust, Axum, and a lot of respect for the ACME protocol
The API lives at api.certy.com.br and it’s written in Rust. This was André’s natural territory and honestly the right call. The backend talks to Let’s Encrypt over the ACME protocol using DNS-01 challenge validation, which means we ask you to add a TXT record to your domain instead of serving a file over HTTP. It works for any domain regardless of your hosting setup.
The session flow is the core of the backend design. When you start a request, the backend creates a session with a random ID. That session lives in PostgreSQL and has a status that progresses through a well-defined state machine:
awaiting_email_verification
-> pending_dns
-> validating
-> issued (then immediately deleted)
-> failed
-> expired Each step requires an explicit API call. Nothing happens automatically. You verify your email with a 6-digit code, then you add the DNS record, then you trigger the DNS pre-check, then you finalize issuance. The certificate comes back in that final response and the session is removed.
The session ID is 48 bytes of cryptographically random data encoded in base64url, which makes it safe to use in URLs and completely unguessable. Small detail, but important.
POST /api/v1/certificates/sessions
POST /api/v1/certificates/sessions/{id}/verify-email
POST /api/v1/certificates/sessions/{id}/dns-check
POST /api/v1/certificates/sessions/{id}/finalize The audit trail is separate from the sessions table. Even after a session is gone, certificate_session_events keeps a record of what happened: session ID, domain, email (hashed), action, IP address, timestamp. This is exactly the kind of logging I wrote about in the “Secure by Design” post. You don’t keep the sensitive data, but you keep enough to reconstruct events if something looks wrong.
The email verification has its own rate limiting baked in: a maximum of 5 code attempts and 3 resends per session, with a 10-minute minimum interval between resends. This exists to prevent abuse, but it also just makes the UX more sensible. If someone’s hammering the resend button, something is wrong.
The frontend: SvelteKit on Cloudflare Workers
The frontend repo is SvelteKit with TypeScript, deployed to Cloudflare Workers via the official adapter. Bun handles the package management and scripts.
SvelteKit was the natural choice here for a few reasons. The routing is clean, the adapter ecosystem for Cloudflare is mature, and the bundle size stays small without much effort. For a tool that’s primarily a multi-step form with some API calls, you don’t need a heavy framework. SvelteKit gets out of the way and lets you build the thing.
The session flow maps directly to routes. The home page starts a session. The /emitir route handles everything after that: email verification, DNS instructions, the pre-check trigger, and the final certificate display. The state lives in the session ID from the URL, which means you can close the tab, come back, paste the URL, and continue exactly where you left off.
/ Home, start session
/emitir Session tracking and flow
/termos Terms of use
/* Custom 404 There’s no user account, no login state, no localStorage. Just a URL. That simplicity was intentional. The less state we manage on the frontend, the less can go wrong.
The proxy: a Cloudflare Worker standing in the middle
The proxy is the piece I’m maybe most quietly proud of, because it’s small and does its job without drama.
The backend runs on a VPS. We don’t want the VPS directly exposed to the internet. So between the Cloudflare-hosted frontend and the backend, there’s a Cloudflare Worker that acts as a strict proxy. It only forwards specific routes, only allows GET/POST/OPTIONS, applies CORS from an allowlist, and injects a shared secret token that the backend requires. If a request doesn’t have the right token, the backend rejects it.
This means the VPS IP is not public. Direct requests to the backend without the proxy token get rejected. The Cloudflare layer handles DDoS protection, TLS termination, and geographic distribution. The backend just processes valid requests from the proxy.
// only these routes get forwarded, everything else gets a 404
const ALLOWED_PATHS = [
'/health',
'/api/v1/certificates/'
]; It’s a short file, TypeScript, deployed alongside the frontend. But the security posture it creates is solid: two layers of validation before anything reaches the actual server.
The git history situation
If you go to the GitHub organization and look at the commit history, you’ll see André’s name on most of the commits. This is a migration artifact. The project lived on a private git server while we built it, and when we moved everything to GitHub, the commit history came over under his account. We both built this. The commit graph just doesn’t show it.
(This is a reminder to always configure user.email correctly when you’re working across git servers. Lesson learned, can’t go back, here we are.)
What I learned building this
Working on Certy was a good reminder that a small, well-scoped tool with a clear purpose is genuinely satisfying to build. There are no clever abstractions for the sake of it, no features that exist because they were fun to write. Every piece of the system exists because the flow required it.
The ACME protocol is more interesting than I expected. DNS-01 challenge validation has this elegant property: it proves domain ownership without requiring your web server to be configured correctly, which makes it work for domains that aren’t serving traffic yet. That’s a real-world use case we wanted to support.
The security details added up to something I’m happy with: no persistent private keys, session-bound state that expires, audit trails that log enough to be useful without logging sensitive data, a proxy that keeps the backend surface area minimal. None of those choices were complicated individually. Together they form a posture.
And Rust for the backend was a good time. The borrow checker is not always a good time. But the resulting binary is fast, the memory safety guarantees are real, and the error handling forced by the type system means the failure modes are explicit in a way that’s actually useful when something goes wrong.
Go try it at certy.com.br. It’s free, it’s open source, and it takes about five minutes. If you hit a bug or have a feature idea, the repos are all public under CertyBR and contributions are welcome.