docs: remove free tier, update rate limits and auth for demo+pro model
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m58s
Promote to Production / Deploy to Production (push) Successful in 2m21s

- Remove free tier from rate limits, add Demo (5/hour, watermarked)
- Update auth section: remove free-tier key mention, link to docfast.dev
- Update getting started: demo → upgrade to Pro → use API key
- Add deprecated: true to /v1/signup/free swagger annotation
- Regenerate openapi.json
This commit is contained in:
DocFast Bot 2026-02-20 19:10:25 +00:00
parent c35ff2bc97
commit 45b5be248c
8 changed files with 1118 additions and 11 deletions

1
dist/index.js vendored
View file

@ -91,6 +91,7 @@ app.use("/v1/demo", express.json({ limit: "50kb" }), pdfRateLimitMiddleware, dem
* /v1/signup/free:
* post:
* tags: [Account]
* deprecated: true
* summary: Request a free API key (discontinued)
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
* responses:

View file

@ -1,7 +1,7 @@
import { Router } from "express";
import rateLimit from "express-rate-limit";
import Stripe from "stripe";
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js";
import logger from "../services/logger.js";
function escapeHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@ -123,6 +123,27 @@ router.get("/success", async (req, res) => {
res.status(400).json({ error: "No customer found" });
return;
}
// Check DB for existing key (survives pod restarts, unlike provisionedSessions Set)
const existingKey = await findKeyByCustomerId(customerId);
if (existingKey) {
provisionedSessions.add(session.id);
res.send(`<!DOCTYPE html>
<html><head><title>DocFast Pro Key Already Provisioned</title>
<style>
body { font-family: system-ui; background: #0a0a0a; color: #e8e8e8; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
.card { background: #141414; border: 1px solid #222; border-radius: 16px; padding: 48px; max-width: 500px; text-align: center; }
h1 { color: #4f9; margin-bottom: 8px; }
p { color: #888; line-height: 1.6; }
a { color: #4f9; }
</style></head><body>
<div class="card">
<h1> Key Already Provisioned</h1>
<p>A Pro API key has already been created for this purchase.</p>
<p>If you lost your key, use the <a href="/docs#key-recovery">key recovery feature</a>.</p>
<p><a href="/docs">View API docs </a></p>
</div></body></html>`);
return;
}
const keyInfo = await createProKey(email, customerId);
provisionedSessions.add(session.id);
// Return a nice HTML page instead of raw JSON

2
dist/services/db.js vendored
View file

@ -146,6 +146,8 @@ export async function initDatabase() {
);
CREATE INDEX IF NOT EXISTS idx_api_keys_email ON api_keys(email);
CREATE INDEX IF NOT EXISTS idx_api_keys_stripe ON api_keys(stripe_customer_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_stripe_unique
ON api_keys(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS verifications (
id SERIAL PRIMARY KEY,

44
dist/services/keys.js vendored
View file

@ -60,21 +60,37 @@ export async function createFreeKey(email) {
return entry;
}
export async function createProKey(email, stripeCustomerId) {
// Check in-memory cache first (fast path)
const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
if (existing) {
existing.tier = "pro";
await queryWithRetry("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]);
return existing;
}
// UPSERT: handles duplicate webhooks across pods via DB unique index
const newKey = generateKey("df_pro");
const now = new Date().toISOString();
const result = await queryWithRetry(`INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (stripe_customer_id) WHERE stripe_customer_id IS NOT NULL
DO UPDATE SET tier = 'pro'
RETURNING key, tier, email, created_at, stripe_customer_id`, [newKey, "pro", email, now, stripeCustomerId]);
const row = result.rows[0];
const entry = {
key: generateKey("df_pro"),
tier: "pro",
email,
createdAt: new Date().toISOString(),
stripeCustomerId,
key: row.key,
tier: row.tier,
email: row.email,
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
stripeCustomerId: row.stripe_customer_id || undefined,
};
await queryWithRetry("INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id) VALUES ($1, $2, $3, $4, $5)", [entry.key, entry.tier, entry.email, entry.createdAt, entry.stripeCustomerId]);
keysCache.push(entry);
// Refresh in-memory cache
const cacheIdx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
if (cacheIdx >= 0) {
keysCache[cacheIdx] = entry;
}
else {
keysCache.push(entry);
}
return entry;
}
export async function downgradeByCustomer(stripeCustomerId) {
@ -86,6 +102,20 @@ export async function downgradeByCustomer(stripeCustomerId) {
}
return false;
}
export async function findKeyByCustomerId(stripeCustomerId) {
// Check DB directly — survives pod restarts unlike in-memory cache
const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE stripe_customer_id = $1 LIMIT 1", [stripeCustomerId]);
if (result.rows.length === 0)
return null;
const r = result.rows[0];
return {
key: r.key,
tier: r.tier,
email: r.email,
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
stripeCustomerId: r.stripe_customer_id || undefined,
};
}
export function getAllKeys() {
return [...keysCache];
}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "docfast-api",
"version": "0.4.1",
"version": "0.4.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "docfast-api",
"version": "0.4.1",
"version": "0.4.3",
"dependencies": {
"compression": "^1.8.1",
"express": "^4.21.0",

1052
public/openapi.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -104,6 +104,7 @@ app.use("/v1/demo", express.json({ limit: "50kb" }), pdfRateLimitMiddleware, dem
* /v1/signup/free:
* post:
* tags: [Account]
* deprecated: true
* summary: Request a free API key (discontinued)
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
* responses:

View file

@ -11,7 +11,7 @@ const options: swaggerJsdoc.Options = {
title: "DocFast API",
version,
description:
"Convert HTML, Markdown, and URLs to pixel-perfect PDFs. Built-in invoice & receipt templates.\n\n## Authentication\nAll conversion and template endpoints require an API key via `Authorization: Bearer <key>` or `X-API-Key: <key>` header.\n\n## Rate Limits\n- Free tier: 100 PDFs/month, 10 req/min\n- Pro tier: 5,000 PDFs/month, 30 req/min\n\n## Getting Started\n1. Try the demo endpoints (no auth required, 5/hour limit)\n2. Upgrade to Pro at [docfast.dev](https://docfast.dev)\n3. Use your API key to convert documents",
"Convert HTML, Markdown, and URLs to pixel-perfect PDFs. Built-in invoice & receipt templates.\n\n## Authentication\nAll conversion endpoints require an API key via `Authorization: Bearer <key>` or `X-API-Key: <key>` header. Get your key at [docfast.dev](https://docfast.dev).\n\n## Rate Limits\n- Demo: 5 conversions/hour, watermarked output\n- Pro tier: 5,000 PDFs/month, 30 req/min\n\n## Getting Started\n1. Try the demo endpoints (no auth required, 5/hour limit)\n2. Upgrade to Pro at [docfast.dev](https://docfast.dev) for clean output and higher limits\n3. Use your API key to convert documents",
contact: {
name: "DocFast",
url: "https://docfast.dev",