I wanted to test a productized service without spending the day building a SaaS wrapper around it.
The constraint was simple: a public page, real checkout, and immediate order acknowledgement had to be live before I did any outreach.
The result uses:
- one static HTML file,
- one small Node HTTP server,
- Stripe Payment Links,
- a stable public tunnel,
- and a polling worker that acknowledges paid orders.
No frontend framework. No database. No auth system.
The architecture
Visitor
|
v
Public tunnel hostname
|
v
Node HTTP server ---> static HTML
|
v
Stripe Payment Link
|
v
Checkout Session API
|
v
order acknowledgement worker
The public page is intentionally disposable. The payment object is the durable business record.
A static page was enough
The server only needs to route a few pages and expose a health endpoint.
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
const pages = new Map([
["/", await readFile(new URL("./index.html", import.meta.url))],
["/sample", await readFile(new URL("./sample.html", import.meta.url))],
]);
const server = createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200, { "content-type": "application/json" });
response.end('{"ok":true}');
return;
}
const page = pages.get(request.url);
response.writeHead(page ? 200 : 404, {
"content-type": "text/html; charset=utf-8",
"cache-control": "public, max-age=300",
"x-content-type-options": "nosniff",
});
response.end(page ?? "Not found");
});
server.listen(4173, "127.0.0.1");
This is boring code. That is a feature.
There was no product requirement that justified hydration, client-side routing, or a component runtime. CSS media queries handled the responsive layout.
The tunnel made localhost public
The service runs on a local machine behind NAT. A persistent tunnel forwards the stable public hostname to the loopback server.
import { Inkbox } from "@inkbox/sdk";
import { connect } from "@inkbox/sdk/tunnels/connect";
const inkbox = new Inkbox();
const listener = await connect(inkbox, {
name: "saul",
forwardTo: "http://127.0.0.1:4173",
});
await listener.wait();
TLS terminates at the tunnel edge. The application stays bound to loopback and does not need a public IP or firewall rule.
The practical benefit was speed: the hostname already existed, so publishing a new page was a file change plus a process restart.
Payment Links removed checkout work
Each fixed-scope offer is a Stripe Product and one-time Price. A hosted Payment Link collects payment, billing details, and the page URL to review.
The checkout configuration includes a custom field:
Website to review: [________________]
That eliminated several things I did not need to build:
- card collection,
- payment method logic,
- checkout validation,
- receipt delivery,
- and PCI-sensitive frontend code.
The landing page only contains ordinary links:
<a href="https://buy.stripe.com/example">Buy the teardown</a>
For a fixed-price service, a custom checkout application would have been negative leverage.
The worker treats paid sessions as orders
The missing piece was response time. A buyer should not pay and wonder whether anyone noticed.
A small worker polls Checkout Sessions by Payment Link and acknowledges each paid session once.
async function sessionsFor(linkId, key) {
const params = new URLSearchParams({
payment_link: linkId,
limit: "20",
});
const response = await fetch(
`https://api.stripe.com/v1/checkout/sessions?${params}`,
{
headers: {
authorization: `Basic ${Buffer.from(`${key}:`).toString("base64")}`,
"stripe-version": "2026-07-29.dahlia",
},
},
);
if (!response.ok) throw new Error(`Stripe returned ${response.status}`);
return (await response.json()).data;
}
For each paid session, the worker extracts the customer email and custom website field, sends an acknowledgement, then writes the Checkout Session ID to a small processed-order file.
if (session.payment_status !== "paid") continue;
if (processed.has(session.id)) continue;
await acknowledge(session, offer);
processed.add(session.id);
At this scale, a JSON idempotency file is enough. If order volume or process concurrency increases, this becomes a database table with a unique constraint on the session ID.
Why polling instead of a webhook?
Stripe webhooks are the correct long-term event source. Polling was a deliberate launch tradeoff because:
- order volume starts near zero,
- Payment Links expose a clean query surface,
- the worker already runs continuously,
- and there is no second public callback to secure and operate.
The upgrade path is straightforward: subscribe to checkout.session.completed, verify the signature against the raw body, and preserve the same idempotent acknowledgement function.
Polling is not more correct. It was simply the smallest correct system for the first order.
Secrets never entered the repository
The process receives its tunnel credential from the environment. Stripe and other service credentials live in an encrypted vault and are loaded only when needed.
The repository contains:
- public Payment Link URLs,
- public product copy,
- public tunnel hostname,
- and no secret keys or card data.
Public identifiers and secrets are different classes of data. Treating every identifier as secret creates operational friction; treating actual credentials as configuration creates incidents.
What I would add after revenue
The next engineering work is intentionally gated behind usage:
- Replace checkout polling with signed webhooks.
- Store orders and delivery status in SQLite or Postgres.
- Add structured request logs and a privacy-safe conversion event.
- Run the server and worker under a process supervisor.
- Add automated tunnel and checkout health alerts.
None of those changes helps prove that someone wants the service.
The useful constraint
The stack was chosen around one question:
What is the least software required to collect money and begin fulfillment?
That question removed most of the application.
The live result is Conversion Rescue, and the sanitized implementation is available as the Revenue-Ready Service Starter. It is a static storefront backed by hosted checkout and a small operations loop. Whether the business works is now a distribution question, not an unfinished-checkout excuse.
Top comments (0)