Fax Her? I Barely Know Her

30p to send me a fax? In this economy? No chance. Here's how I built a modern-day, paid fax machine for my portfolio site.

13/08/2026

Generating
Generating a smart summary for this post.

Prologue

Back in my day... fax was mostly dead. The big (dot).com bubble of the 2000's meant a lot of businesses had moved away from fax as their main communication method, mostly relying on the new and shiny internet FTP sites to host shared files for them.

But regardless of its lack of use, fax is still, to this day, an awesome technology. Talk about making use of the tools you have available; much like dial up, fax converted data into audible tones transmitted over a phone call. A PHONE CALL. As cool as this may be though, my "fax" system doesn't rely on traditional telecom infrastructure. My take is a bit more modern.

Motivation

Wouldn't it be cool if people could get in touch with me via fax? Absolutely. I already have a printer, homelab, and remote access system setup, so what's stopping me really? Ink. £40 for a replacement ink cartridge is diabolical, so making it free would hit my wallet hard.

Planning & Architecture

That's where Stripe comes into play. If you want a payment processor with excellent DX, Stripe is the way to go. But another issue arises, fees. I originally wanted my "fax" system to only be 10p per page, but Stripe's foundational fees are 1.5% + 20p per transaction, so I guess 30p will have to do (sorry).

So with the Ink situation remedied, its time to build the actual "fax" system.

Fax Diagram Note: Diagram doesn't go into full depth, just the basics.

  • Running locally on my homelab:

    • Docker
      Containerization software for deploying applications.
      • Cloudflare Tunnel (x2)
        For secure remote access without port forwarding.

      • Tailscale
        Similar to Cloudflare Tunnel, acting as an emergency failover remote access solution.

      • CUPS
        Open source printing system developed by Apple.

  • Running in the cloud:

Implementation

The Printing Server

On my homelab, I'm hosting a CUPS server in a Docker container, with the host physically connected to the printer via USB.

Since I have a Canon printer, the most popular CUPS Docker image (ydkn/cups) does not include the drivers out the box, so for ease I dropped in the manuelklaer/cups-canon replacement image, with the Canon drivers preinstalled.

A general rule of thumb when working with Docker containers and reverse proxy services like Cloudflare Tunnel, give each container a hostname, so you can route internal traffic to it easier, for example cups:631.

Configuring the Remote Access

My homelab connects to the public internet via a Cloudflare Tunnel, with a few extra steps between. The actual waterfall looks something like:

Public Hostname ➔ Cloudflare Access ➔ Cloudflare Load Balancer ➔ Cloudflare Tunnel (Primary) / Cloudflare Tunnel (Secondary) / Tailscale (Emergency Failover)

As shown above, I utilise a managed cloud load balancer to offer failover to different connection routes to my homelab in case of degradation.

Because of this, all the routes have to be configured to resolve the CUPS server public hostname to the internal Docker container.

Before this can be done though, its imperative that the Cloudflare Access rule is configured ahead of any service routing. To connect to the CUPS server, I'm using a Cloudflare Access "Service Auth" rule, which bypasses any Cloudflare login GUI's, acting like a second header-based API key.

Connecting the API's

My portfolio is hosted on Cloudflare Workers, on a separate domain from my homelab. So in order to allow the server to receive the "fax", the Next.js (Cloudflare) Worker must have its own print API, authenticated with the Stripe Payment Intent ID to ensure unpaid API requests cannot be sent.

Since the public-facing API is hosted on a Cloudflare Worker, we can utilise the Worker's rate limiter binding to offer simple rate limiting alongside the standard text validation and character limits. Both the payment intent and print API limit at the same rate, with a fixed maximum of 2 requests per minute.

To ensure compatibility, the best way to render print jobs is as a PDF. So in the same API route, text input provided to it is converted into a PDF using pdf-lib before sending to the CUPS server, after validation of the Stripe Payment ID.

Print Job Received ➔ Validate Payment ID ➔ Basic Text Validation (max character limit, etc) ➔ Conversion to PDF ➔ Send to CUPS ➔ Failed? Retry ➔ Failed Again? Initiate Refund 

Although my homelab boasts an uptime of 99%, my printer on the other hand, may not. So to ensure I don't receive any angry emails asking for refunds, the API will automatically issue one after two failed printing attempts.

Conclusion

After a couple hours debugging printer driver issues, and a printer factory reset later, it didn't turn out too bad. A fun little side project, and if you have the infrastructure ready to go, I highly recommend building something similar. Happy faxing!

Wanna try?

Send me a fax via the contact form below, let me know what you think!

£0.30

Send a Fax

Free

Send me a message