Security
This page lists what the product does, in enough detail that you can check it.
Signed render URLs
Every image URL carries an HMAC-SHA256 signature over the path and the canonical query string, verified before anything renders. Nobody can change a deadline, a merge value or a template id in a delivered URL and get a different picture back.
Signing keys are per workspace rather than global, and a key can be rotated from the dashboard. The new key signs immediately and the previous key keeps verifying, so URLs already in delivered inboxes keep rendering. Revoking the retired keys is a separate, deliberate action, and it is the kill switch for every outstanding URL in that workspace.
Tenant isolation
Every query that touches workspace data goes through a repository layer that requires a workspace id. The API is the boundary; the dashboard only hides buttons.
The test for it is generated, not hand written. It discovers every route on disk, treats any route carrying a workspace id as tenant scoped, and calls each one signed out, as a member, and with a perfectly valid session belonging to a different tenant. The cross-tenant case requires a 404 rather than a 403, so the API never confirms that another tenant's object exists, and it also asserts that nothing but the membership check ran: the route never reached the data at all.
Accounts and sign-in
Passwords are hashed and salted and the plaintext is never stored. Email verification is required before an account works, and sign-up returns the same response whether or not the address was already registered, so the form cannot be used to enumerate customers.
Cloudflare Turnstile sits on sign-up, sign-in, forgotten password and password reset. Rate limits run on two axes: per IP and path, so one host cannot grind through many accounts, and per submitted email address, so many hosts cannot grind through one. Sessions are opaque tokens lasting 14 days, reset links 30 minutes, verification links 60.
API keys are shown once, at creation. What we store is a SHA-256 hash plus the non-secret prefix, so a leak of that table yields no working key. Calls are rate limited per key with a Redis token bucket, 60 requests a minute and 600 on the batch URL route.
Recipient data in URLs
You do not have to put a name or an email address in an image URL. Upload a recipient list and every row gets
an opaque token, so the URL carries ?r=tok_... and the merge values are looked up server side.
Uploads and outbound fetches
A template's image layers can only point at an asset you uploaded. There is no field for an arbitrary URL, so the render path cannot be aimed at an internal address or a third-party host.
Uploads are identified by sniffing the bytes rather than trusting the declared type or the filename, and anything that does not read as a PNG, JPEG, GIF or WebP is refused. The caps are 5 MB a file, 6,000 pixels on a side, 25 million pixels in total, and 500 assets or 200 MB a workspace.
The service that answers inbox requests has no database connection at all, by architectural contract. Its dependencies are the render engine and Redis.
Backups and dependencies
Postgres is dumped hourly to a dedicated Cloudflare R2 bucket with 30 day retention, using a token that cannot read the rest of the account. Restoring is a script: it pulls the latest dump, restores into a scratch container and checks row counts. That drill has run once, before the current tables existed, so a drill against the full schema is still outstanding.
CI runs a dependency audit on every push and fails the build on high and critical advisories, including advisories published against dependencies nobody touched.
What we do not have
We do not hold SOC 2 or ISO 27001. We are a small company and that audit is not something we can honestly claim. What is on this page is what we actually do, and you are welcome to ask us anything this page does not answer.
Reporting a vulnerability
Write to support@emailtimer.app with "Security" in the subject line, and enough detail to reproduce: a proof of concept URL, a request and a response, or a short script.
You get an acknowledgement within one business day, then an assessment: whether it reproduces, what the fix is, and when it ships. You get told again when it is deployed. Please do not test against another customer's workspace or data; make a second account of your own and attack that. There is no paid bounty programme. We will credit you here by name if you want, and we ask for 90 days before you publish.
The agreements that go with this page
The privacy policy says what is held and for how long. The data processing addendum names every sub-processor and carries the breach timeline.