bob/new proposal foracme corp, 4k, duefridaytelegramclaude codeproposals/acme-corp.md# Acme Corp Proposal## Scope- Build proposals portal MVP- Telegram intake -> markdown- Viewer at /proposals/<id>## Timeline- Due friday## Price$4,000telegram -> claude code -> proposal document
· 8 min read ·

Telegram, Claude Code, and a Proposals Portal by Midnight

Magic link auth, progress tracking, and one-command publishing. Built with Claude Code, Astro, Cloudflare KV, and Resend from my phone.

dev ai claude cloudflare astro

My girlfriend runs a piercing studio. She orders jewelry from five different vendors, manually updates her Square inventory, and tracks everything in her head. I brainstormed a spec with Claude Code to automate the whole thing. Parseur for invoice parsing, Make.com for automation, Square API for inventory updates. Solid spec, ready to build.

But then I realized something. I had no way to show her the spec. Email a markdown file? Print it? Read it aloud over dinner?

I needed a way to deliver proposals to clients. Something that looked professional, tracked progress, and didn’t require me to set up a Google Doc every time. So I built one. From my couch. Via Telegram.

What Got Built

A full proposals portal on my tech site. Clients enter their email, get a magic link, click it, and see their proposal rendered with full markdown styling. Dark theme, tables, code blocks, architecture diagrams. Everything looks clean.

But the real feature is the progress tracker. A sticky sidebar with every deliverable listed as a checklist item. Two columns: one for the client to approve steps, one for me to mark them complete. Real-time updates. Timestamps. A progress bar that fills as work gets done.

No accounts. No passwords. No sign-up forms. Just email, click, done.

And when I want to share a proposal with someone who isn’t in the system? A Share button generates a one-time link. Copy it, text it, send it however. Works once, expires in 24 hours.

How It Works

Architecture stays simple on purpose. Astro renders proposal markdown through its content collection system. SSR pages handle authentication. Cloudflare KV stores everything stateful: tokens, sessions, access control, checklist data.

Magic link flow works like this. Client visits /proposals, enters their email. A POST to /api/proposals/send-link checks KV for an access entry. If found, generates a UUID token, stores it in KV with a 24-hour TTL, and fires off an email via Resend’s API. Always returns the same “check your email” message regardless of whether the email exists. No enumeration.

Client clicks the link. SSR page validates the token (one-time use, deleted after read), creates a session in KV with a 7-day TTL, sets an HttpOnly cookie, and redirects to the proposal. Every subsequent page load checks the session cookie against KV. If it’s expired or missing, back to the email form.

Checklist state lives in KV as a JSON object per proposal. Each step has four fields: clientApproved, clientApprovedAt, bobCompleted, bobCompletedAt. Client clicks “approve” on a step, it posts to an API endpoint, KV updates, page reloads with the new state. I click “complete” on my side. Both tracks are independent. When everything lines up, status flips to “Completed” with a filled progress bar.

Rate limiting uses KV too. A counter key like ratelimit:sendlink:192.168.1.1 with a 5-minute TTL. Five requests max per window. Cheap, effective, no external service.

Total monthly cost: $0. Cloudflare KV free tier gives 100,000 reads and 1,000 writes per day. A proposals portal for a handful of clients uses maybe 0.01% of that. Resend free tier handles 100 emails per day. Already running on Cloudflare Workers for the site itself. Zero new infrastructure costs.

What Made It Fast

I built this via Telegram. Not SSH. Not a laptop. Claude Code running at home, me sending messages from my phone.

Started with brainstorming. “Let’s build a proposals page on the tech site.” Claude asked questions one at a time, multiple choice where possible. What kind of auth? How should the checklist work? Where should data live? How do you want to create proposals?

Nine questions later, we had a design. Claude wrote the spec, committed it, and started writing the implementation plan. 11 tasks, full TDD, exact file paths, complete code blocks. Every test written before the implementation.

Then subagent-driven development kicked in. Claude dispatched a fresh agent per task. KV helpers got their own agent with 13 tests. Auth helpers got 6. API endpoints got their own agents with test suites. Each agent implemented, tested, committed, and reported back.

I watched progress updates roll in on Telegram while the agents worked. Like a build dashboard, but for feature development.

Nine commits. 38 tests. 19 new files. 1,542 lines of code. All passing.

First deploy had a bug. Astro’s redirect() method drops custom headers, so the session cookie wasn’t being set. Two minutes to diagnose, fix, and redeploy. Second deploy had a SameSite=Strict cookie that blocked clicks from email (Gmail is a different origin). Another quick fix.

Normal iterative development, just moving faster because the feedback loop is tight. Third issue was the cookie’s Path being scoped to /proposals while the API endpoints lived at /api/proposals/. Browser wouldn’t send the cookie to a different path prefix. Broadened to / and everything connected.

Three bugs in a brand new auth system. All found by testing in production (the only environment that matters for auth flows). All fixed within minutes because the deploy pipeline runs in 60 seconds.

One-Command Publishing

After the portal shipped, I needed to actually publish the piercing shop proposal. So I built a Claude Code slash command: /proposal-publish.

Give it a spec file path and a client email. It reads the spec, extracts the title and deliverables, generates a proposal content file with proper frontmatter, seeds the KV entries (client access, admin access, checklist state), commits, pushes, waits for CI, and sends the magic link.

One command. Spec to live proposal in about 90 seconds.

My girlfriend got the email, clicked the link, and saw her proposal rendered on a dark, clean page with a progress tracker showing 0/7 steps complete. Professional enough to show to anyone. Built in an evening.

Stack

For anyone who wants to build something similar:

  • Astro 5 with per-page SSR (prerender = false on auth-gated pages)
  • Cloudflare Workers KV for stateful data (tokens, sessions, access lists, checklists)
  • Resend for magic link emails (free tier, 100/day)
  • Tailwind Typography for markdown rendering (same prose classes as the blog)
  • Vitest for testing (38 tests across 5 files)
  • Claude Code for implementation (subagent-driven development with TDD)

KV is doing a lot of work here. Tokens with TTL for auto-expiry. Sessions with TTL. Rate limiting counters with TTL. Access control lists. Checklist state. All in one namespace, all free tier.

Security Pass

Before calling it done, I ran a full security audit. An Opus-level agent read every file and checked for auth bypasses, injection vectors, CSRF vulnerabilities, information leakage, and rate limiting gaps.

Found three high-priority items:

  1. Anonymous share access entries weren’t expiring (KV pollution over time)
  2. No rate limiting on the magic link endpoint (email bombing risk)
  3. No rate limiting on share link generation

Also found medium issues: weak email validation, no CSRF origin checks, no slug format validation. Fixed all seven in one commit. Added KV-based rate limiting (5 requests per 5 minutes per IP), origin header validation on all POST endpoints, and TTLs on anonymous access entries.

Security audit to fix deployment: about 15 minutes.

What I’d Do Differently

Not much. But if I were building this for a paying client instead of myself, I’d add a proposals list page for clients with multiple active proposals. Right now, the magic link drops you straight to your first proposal. Fine when you have one. Less fine when you have three.

I’d also shorten the magic link TTL from 24 hours to 2 hours. Industry standard for magic links skews shorter because of email interception risk. Single-use tokens mitigate this, but a shorter window is better defense-in-depth.

And I’d move clientEmail out of the markdown frontmatter. It’s not rendered anywhere, but it’s in the build output. Access control already lives in KV where it belongs. Putting the email in the content file is redundant and creates a latent exposure risk if someone accidentally renders proposal.data.clientEmail in a template.

Build What You Need, Nothing More

I’m not a SaaS company. I don’t need a proposal tool with 47 integrations and a monthly subscription. I need to send a spec to my girlfriend and track whether we’ve finished setting up Parseur.

Building exactly what you need, nothing more, is the whole point. And the speed at which you can do it now makes throwaway internal tools viable. This entire system, from brainstorming to deployed and security-hardened, happened in one session.

Next time I brainstorm a project for a client, I won’t email a markdown file or screen-share a Google Doc. I’ll run /proposal-publish, and they’ll get a magic link to a page that looks like it belongs to a real software company. Because it does.

If you’re building client-facing tools and want to swap notes, reach out.