DEV Community

Cover image for I built an e-signature app that charges you for storage, not signatures
Akbar Ali
Akbar Ali

Posted on

I built an e-signature app that charges you for storage, not signatures

The bill that made no sense

A signature is about 400 bytes of JSON and a hash.

Every big e-signature product will charge you somewhere between $1 and $4 to store those 400 bytes. They call it an "envelope". Run out of envelopes on the 22nd of the month and your deal waits until the 1st. I have watched a real contract sit still because a counter hit zero.

The expensive part is the other thing. A 4 MB PDF sitting in storage for seven years, backed up and replicated, costs real money every month. That part is free. It is a checkbox on the feature list.

So the pricing is backwards. It charges for the cheap thing and gives away the expensive one.

I built Putmysign the other way around. Unlimited documents, unlimited recipients, unlimited signatures, on every plan. You only pay if you want the files kept.

Free keeps your PDFs for 5 days. Pro is $19 a month and keeps them forever. The audit trail is kept forever on both, because it is a few kilobytes and it is the whole point of the product.

That one pricing decision shaped almost every technical choice underneath. That is the interesting part.

1. Deleting files is a feature, so it has to actually happen

If I promise free files are gone in 5 days, "we will get to it eventually" is not good enough. A scheduled job walks every document past its expiry date and removes the file from storage, along with every cached page image made from it.

But it deletes the file, not the record. The row stays. The audit trail stays. The hashes stay:

const originalHash = sha256(originalBytes);
// signatures applied, certificate page added
const finalHash = sha256(finalBytes);
Enter fullscreen mode Exit fullscreen mode

So five days later the PDF is gone from my storage and I still cannot lie about what happened. If you kept your own copy, you can hash it and check it against a record I cannot quietly change.

Throwing away the file while keeping the proof turned out to be a nice property. I did not design it. It fell out of the pricing.

The worst bug in this area was a quiet one. A signing link can outlive the file it points to. The recipient clicks a link that is perfectly valid, and lands on nothing. So link expiry is capped to the retention window:

/**
 * A share link must never outlive the file it points to, otherwise a recipient
 * opens a valid token and finds nothing to sign.
 */
export const MAX_EXPIRY_DAYS = PLAN.retentionDays;
Enter fullscreen mode Exit fullscreen mode

Two separate clocks on the same object will drift apart sooner or later. Make one of them depend on the other.

2. Recipients never make an account

Unlimited recipients means a recipient cannot be a user record I charge for.

A signup wall in the middle of someone else's contract is the biggest reason paperwork stalls. It mostly exists so vendors can count seats.

So the signing link is the login. It is an HMAC over the document and the recipient, built from a server-only secret. Only the hash of the token is stored:

const secret = process.env.SIGNING_TOKEN_SECRET ?? process.env.SESSION_SECRET;
if (secret.length < 32) throw new Error("must be at least 32 characters");
return createHash("sha256").update(token).digest("hex");
Enter fullscreen mode Exit fullscreen mode

Store the hash, not the token. If my database leaks tomorrow, nobody gets a working signing link out of it. Same idea as hashing passwords, applied to a URL.

3. One row per document

A signing session is a burst of tiny writes. Opened. Viewed page 3. Signed field 2. IP recorded. All of it against one document that is only ever read as a whole.

Splitting that across six tables buys me joins I never run. So each document is one JSONB column holding everything, with the fields I actually filter on copied out into real indexed columns: owner, status, updated date, expiry date, purge date. A GIN index on the JSON handles signing-link and "shared with me" lookups.

The real risk with a single row is two people signing at the same moment. Last write wins, one signature disappears. I fixed it the boring way, with SELECT ... FOR UPDATE inside a transaction.

Postgres has been a fine document database for years, and it still hands you row locks when you need them.

4. Thumbnails without a thumbnail service

Page previews come from pdftoppm, part of poppler. It is a 25 year old C program. I shell out to it, render the page when someone asks for it, and cache the image back into the bucket next to the PDF. Delete the PDF and the cached pages go with it.

No render service, no queue, no third party API with a per page price on it. Storage is what I charge for, so anything that turns compute into a monthly bill works against the model.

The stack, quickly

React Router 7, TypeScript, Postgres with Prisma, any S3 compatible bucket (MinIO locally, so the whole thing runs on a laptop with one docker compose up), Firebase Auth for owners only, Resend for email with bounce handling, Paddle for billing.

Nothing fancy. The interesting decisions are all about what is missing.

Things worth stealing

You do not have to care about e-signatures to use any of this:

  1. Charge for what actually costs you money. If your pricing unit and your cost are different things, people feel it as unfair long before they can explain why.
  2. Delete the file, keep the proof. Hashes and audit rows are tiny. They let you delete aggressively and still stay accountable.
  3. Two clocks on one object will drift. Derive one from the other.
  4. A signup wall in someone else's workflow is a tax on your own customer. They pay it in deals that stall.
  5. JSONB, plus a few copied-out indexed columns, plus FOR UPDATE covers a lot of "we need a document database" situations.

It is live at putmysign.com. Free tier, no card. Upload a PDF you already have and send it to someone who will never make an account.

If you can break the signing link logic, I would genuinely like to hear about it in the comments.

What is the most backwards pricing unit you have run into? I will start: per seat pricing on a read only viewer.

Top comments (1)

Collapse
 
koda2026 profile image
Harun - solo dev

The "delete the file, keep the proof" idea is brilliant — the hashes are a few bytes but the accountability is permanent. And "two clocks on one object will drift, derive one from the other" is going straight into my notes.

To answer your question: the most backwards pricing I've personally hit is per-email limits on auth. I'm 12 and I built an AI coding mentor app entirely on my Android phone (free tiers only). On launch day I hit the Supabase confirmation-email rate limit — gating the exact email that lets a user INTO the product felt exactly like your envelope story. Charging for the cheap thing, blocking the important thing.

Also huge respect from a fellow Indian builder (I'm in Tamil Nadu). Storing the hash of the HMAC token instead of the token itself is the kind of detail I'm trying to learn more of as I grow.

I'll play with the free tier this weekend and try to break the signing link logic. If I find anything, I'll report back. 🚀