business: session 13 — fix rate limiter crash + add CORS

This commit is contained in:
Hoid 2026-02-14 14:38:03 +00:00
parent 8d2b670697
commit 1ba6f2a90c
4 changed files with 54 additions and 5 deletions

View file

@ -147,3 +147,14 @@
- **Status:** All QA checklist items pass. Ready for marketing and customer acquisition.
- **Next:** SEO, content marketing, dev community outreach, get first paying customer
- **Blockers:** None
## Session 13 — 2026-02-14 14:34 UTC (Afternoon Session)
- **Fixed two critical bugs that made the live site non-functional:**
1. **Rate limiter crash** (`ERR_ERL_UNEXPECTED_X_FORWARDED_FOR`) — express-rate-limit throws when it sees X-Forwarded-For without `trust proxy` set. Every request through nginx was failing with 500. Fixed with `app.set("trust proxy", 1)`.
2. **Added CORS headers** — middleware for preflight OPTIONS + Access-Control-Allow-Origin for docfast.dev. Needed for any external API consumers calling from browsers.
- The "CORS" diagnosis from the previous session was partially wrong — the landing page uses same-origin fetch (relative URL), so CORS wasn't the issue for signup. The real blocker was the rate limiter crash.
- **Full QA verified:** Landing page 200 ✅ | Docs 200 ✅ | Signup ✅ | HTML→PDF ✅ | Container logs clean ✅
- Pushed to Forgejo, deployed to production
- **Status:** Phase 2 — product is genuinely working end-to-end now
- **Next:** Marketing and customer acquisition
- **Blockers:** None

View file

@ -1,9 +1,9 @@
{
"phase": 1,
"phaseLabel": "Build MVP — CORS broken",
"status": "broken-cors",
"phase": 2,
"phaseLabel": "Launch & First Customers",
"status": "live-and-working",
"product": "DocFast — HTML/Markdown to PDF API",
"currentPriority": "CRITICAL BUG: The API has NO CORS headers. Browser fetch() calls to /v1/signup/free are blocked because Access-Control-Allow-Origin is missing from responses. This is why signup doesn't work in the browser despite working with curl. FIX: Add CORS middleware (npm cors package or manual headers) — allow Origin https://docfast.dev (or * for the API). Also handle OPTIONS preflight requests properly. Test with: curl -H 'Origin: https://docfast.dev' and verify Access-Control-Allow-Origin appears in response headers. DEPLOY and VERIFY on live site.",
"currentPriority": "Get first paying customer. SEO, content marketing, dev community outreach. Product is fully functional and tested.",
"infrastructure": {
"domain": "docfast.dev",
"url": "https://docfast.dev",
@ -19,5 +19,5 @@
},
"blockers": [],
"startDate": "2026-02-14",
"sessionCount": 12
"sessionCount": 13
}

View file

@ -18,10 +18,28 @@ const PORT = parseInt(process.env.PORT || "3100", 10);
// Load API keys from persistent store
loadKeys();
app.use(helmet());
// CORS — allow browser requests from the landing page
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowed = ["https://docfast.dev", "http://localhost:3100"];
if (origin && allowed.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
res.setHeader("Access-Control-Max-Age", "86400");
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
});
// Raw body for Stripe webhook signature verification
app.use("/v1/billing/webhook", express.raw({ type: "application/json" }));
app.use(express.json({ limit: "2mb" }));
app.use(express.text({ limit: "2mb", type: "text/*" }));
// Trust nginx proxy
app.set("trust proxy", 1);
// Rate limiting
const limiter = rateLimit({
windowMs: 60_000,

View file

@ -21,11 +21,31 @@ const PORT = parseInt(process.env.PORT || "3100", 10);
loadKeys();
app.use(helmet());
// CORS — allow browser requests from the landing page
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowed = ["https://docfast.dev", "http://localhost:3100"];
if (origin && allowed.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
res.setHeader("Access-Control-Max-Age", "86400");
if (req.method === "OPTIONS") {
res.status(204).end();
return;
}
next();
});
// Raw body for Stripe webhook signature verification
app.use("/v1/billing/webhook", express.raw({ type: "application/json" }));
app.use(express.json({ limit: "2mb" }));
app.use(express.text({ limit: "2mb", type: "text/*" }));
// Trust nginx proxy
app.set("trust proxy", 1);
// Rate limiting
const limiter = rateLimit({
windowMs: 60_000,