Shipping a private, paywalled web app with no budget

Words
552
Reading
3 min
15m
Listen to this post
~4 min

With Claude's massive support, I developed a regulatory monitor for people who want to trade legally on the US.-Cuba route, a.k.a. the Cuba Sanctions Hub. As you already know, it is a Node/Express app, plus a sister Streamlit app exclusively for Census trade data. My goal over the past two days has been to deploy the hub online and monetize access. My constraints were harsh: private code, zero budget, and every screen behind a login. Here is how it went, including what Claude got wrong the first time.

Who did what

Claude wrote the code, tests, and docs, although it cannot test against the actual API, or push or deploy for me. In this sense, Claude's commits carry an explicit trailer, which I like because it makes authorship honest:

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

GitHub

Two private repos, SSH authentication from my MX-Linux box. Pushes failed first with "Repository not found" (a wrong repo name, not a permissions problem), then worked.

The Render wall

Render was the first target. Its "Free" tier still demands a credit card, and I found no documented way around it. Neither Claude. Borrowing a friend's card is a bad idea --I want to achieve things without owing anything to anyone--, and an overage billing risk on a zero-budget project is a non-starter. We dropped Render rather than hack around it. SnapDeploy's free tier needs no card: 0.25 vCPU --even better than Render on this--, sleeps after 15 idle minutes, 100 awake hours a month, ephemeral disk.

Claude's mistakes (all fixed, none hidden)

  • The Dockerfile still exposed Render's port 10000; SnapDeploy manages PORT itself and rejected my env var. Fix: EXPOSE 3000, no PORT variable.

  • A comment placed inline inside a JS object swallowed the properties after it and broke two tests.

  • Login returned a 503 for correct passwords. The server log said [auth] perfil 403: I had disabled Supabase's automatic table exposure, so the perfiles table needed explicit grants.

grant usage on schema public to authenticated;
grant select on public.perfiles to authenticated;

Supabase: access control and monetization

Quick config here is Supabase Auth plus one table with row-level security. Signups are disabled, so I create users and activate plans manually (free Supabase email only reaches team members, about two per hour). Each profile has plan (generico or pago), activo and vence. The server verifies the token on every request, fails closed with a 503 if auth is down, and refuses to start on a public host without Supabase. Sessions live in HttpOnly cookies, and the publishable key never reaches the browser's JavaScript.

The generic plan browses everything read-only. The paid plan unlocks downloads, enforced on the server:

if (/^\/auth\//.test(pathname)) return true;                  // login/logout always allowed
if (plan !== 'pago') return method === 'GET' && !PAID_ONLY.some(re => re.test(pathname));

On the user side, the browser only shows a courtesy popup. Idle sessions now close after 60 minutes, tracked server-side. Payment-processor terms are noted, not legal advice.

Added while deploying

  • Optional toggle for unpublished Census corrections: they conserve total value, skip anything the API already revised, and report what was omitted.

  • A fix for an OFAC download failing with CERTIFICATE_VERIFY_FAILED: adding ca-certificates to the image. Unverified in production, and a TLS-inspecting proxy could still be the cause.

  • A "Partners in the US" section (Council lists, USDA directories). The Council PDFs date from 2019–2020, so the app shows dates and drops companies I know were absorbed or dissolved.

Tomorrow will be a big day for this effort, as I will try to sell the Hub's services to Cuban economic actors. Will see.

Shipping a private, paywalled web app with no budget | Ecency