Most of the time, “store this document and remember when it was created” is a solved problem: a database row, a timestamp column, done. So why put a smart contract in front of it at all?

This post explains why I built the Document Notary on a blockchain, how it works today, and the trade-offs that came with that choice.

The trust problem a database can’t solve

A timestamp in a database is only as trustworthy as the people who control that database. If I run the database, I can edit the timestamp. If you run it, you can edit it. Notarization exists because the parties involved sometimes don’t fully trust each other. Nor do they always trust a third party’s server not to be quietly modified, hacked or subpoenaed. A regular CRUD app can prove “a row says this”, never “this couldn’t have been changed”.

A public blockchain changes that. Once a hash is written to the chain, changing it means rewriting history on a ledger that many independent computers hold copies of. That’s a fundamentally different guarantee from “trust my server”, and it applies to me as the operator too.

How a notarization works

The app does two jobs: notarize a file, and verify one later.

To notarize, a signed-in user picks a file, and the browser shows its fingerprint before anything is sent. After upload, the backend computes the file’s SHA-256 fingerprint and calls the contract’s notarize function with that fingerprint. The contract stores it with the block’s timestamp and refuses duplicates. The file itself is kept privately, and the record is indexed so the owner can find it again.

To verify, nobody needs an account or has to upload anything. The browser hashes the file locally and sends only the fingerprint to the backend. The backend asks the contract whether that fingerprint exists and when it was recorded. If a single byte of the file has changed, the fingerprint is completely different and the check fails.

That asymmetry is deliberate. Notarizing needs an account, because it costs gas and needs limits. Checking a proof should be possible for anyone, with nothing to sign up for.

What goes on-chain, and what doesn’t

Everything passed to notarize is public and permanent, so the app sends as little as possible:

  • On-chain, public: the document’s hash, the block timestamp, the address that submitted it, and an optional label. The label is empty unless the user ticks a box and types one, up to 64 characters, with a warning that it can never be edited or deleted.
  • Off-chain, private: the original file lives in a private Cloudflare R2 bucket. Only the account that notarized it can get a short-lived download link. The original filename, file size and owner live only in Supabase, and public API responses never include them.

One honest correction from the project’s history: early versions sent each file’s original name as the on-chain label. Those older records still carry their filenames, and that can’t be undone. It’s a good reminder that on a blockchain, “we’ll fix it later” doesn’t apply to data you’ve already published.

A hash alone doesn’t reveal what a file says. But anyone who already has a candidate file can check whether it was notarized, so a hash isn’t a secret for small or guessable documents.

The relayer: no wallet needed, at a price

I didn’t want users to need a crypto wallet or to buy ETH just to timestamp a PDF. So the backend holds one funded wallet, the relayer, and submits every transaction on users’ behalf.

That convenience has a cost in what the chain can prove. The on-chain submitter is always the relayer, never the end user, so the chain doesn’t say who uploaded a file. Which account notarized what is recorded off-chain only, in the operator’s database. Users trust the operator to submit what they uploaded, though they can check that themselves at any time with the Verify tab.

Because the relayer spends real gas, the backend also guards the wallet. Each plan has a monthly quota and file-size limit. A daily spending budget and a per-transaction fee cap are enforced inside Postgres, so they hold even with several backend instances. Every attempt is tracked as an operation with a clear state, so a retry never sends a second transaction or uses quota twice.

What a proof does and doesn’t establish

This is where overselling a blockchain product is tempting, and where I’ve tried hardest not to.

  • Integrity: a file whose fingerprint matches the record is byte-for-byte identical to what was notarized.
  • Existence by a time: the file existed no later than the recorded block time.
  • Not authorship. Whoever holds a file can notarize it.
  • Not “first”. It proves existence by a time, not that nobody had the file earlier.
  • Not a legal notarization. No identity check or legal witness is involved.

I wrote more about where this matters in practice, for journals that need to prove when a paper arrived, in Manuscript timestamping for journals.

A proof only helps if you can show it to someone. Owners can now download a one-page Proof of Existence Certificate for any confirmed notarization. It shows the full hash, the recorded time in UTC, the network, the contract and transaction, and a QR code. The original filename appears only on the owner’s own copy.

The QR code opens a public verify page for that hash, which anyone can use without an account. The certificate isn’t the evidence itself, since anyone could edit a PDF. What makes it trustworthy is that every value on it can be checked against the chain, and it includes the steps to do that.

Why several platforms

Vercel serves the React frontend and Render runs the Node.js backend. Supabase handles sign-in, plans and the records index, Cloudflare R2 stores the private files, and the contract provides immutability. It would have been easier to cram everything into one deploy. But keeping “serve the UI”, “handle app logic”, “store files” and “provide immutability” separate made each piece easier to reason about and easier to swap later.

That separation paid off when it came to reliability. Verification reads the chain first and treats the database as optional, so a paused database doesn’t break the ability to check a proof.

The honest trade-offs

The notary currently runs on Sepolia, an Ethereum test network. That’s fine for demonstrating the mechanism, but test-network ETH has no value and the network’s future is up to Ethereum’s developers. So today’s proofs shouldn’t carry legal or commercial weight. The next step is moving to a low-cost main network so proofs are permanent, which also means a real gas budget.

Even then, a blockchain isn’t free. Gas costs money, the relayer is a trust point users have to accept, and a public chain means the hash is visible to anyone. For many use cases, a signed database record with an audit log is genuinely fine. This project exists for the narrower slice of cases where “genuinely fine” isn’t good enough, where the record has to hold up even if you don’t trust the person running the server. Building it was a good way to feel those trade-offs rather than just read about them.