diff --git a/dist/middleware/usage.js b/dist/middleware/usage.js index 180f5a7..7350a40 100644 --- a/dist/middleware/usage.js +++ b/dist/middleware/usage.js @@ -2,6 +2,7 @@ import { isProKey } from "../services/keys.js"; import logger from "../services/logger.js"; import pool from "../services/db.js"; const FREE_TIER_LIMIT = 100; +const PRO_TIER_LIMIT = 5000; // In-memory cache, periodically synced to PostgreSQL let usage = new Map(); function getMonthKey() { @@ -36,6 +37,15 @@ export function usageMiddleware(req, res, next) { const key = keyInfo?.key || "unknown"; const monthKey = getMonthKey(); if (isProKey(key)) { + const record = usage.get(key); + if (record && record.monthKey === monthKey && record.count >= PRO_TIER_LIMIT) { + res.status(429).json({ + error: "Pro tier limit reached (5,000/month). Contact support for higher limits.", + limit: PRO_TIER_LIMIT, + used: record.count, + }); + return; + } trackUsage(key, monthKey); next(); return; @@ -46,7 +56,7 @@ export function usageMiddleware(req, res, next) { error: "Free tier limit reached", limit: FREE_TIER_LIMIT, used: record.count, - upgrade: "Upgrade to Pro for unlimited conversions: https://docfast.dev/pricing", + upgrade: "Upgrade to Pro for 5,000 PDFs/month: https://docfast.dev/pricing", }); return; } diff --git a/dist/routes/billing.js b/dist/routes/billing.js index 647a512..e99f834 100644 --- a/dist/routes/billing.js +++ b/dist/routes/billing.js @@ -67,7 +67,7 @@ a { color: #4f9; }

Your API key:

${escapeHtml(keyInfo.key)}

Save this key! It won't be shown again.

-

10,000 PDFs/month • All endpoints • Priority support

+

5,000 PDFs/month • All endpoints • Priority support

View API docs →

`); } @@ -171,7 +171,7 @@ async function getOrCreateProPrice() { else { const product = await getStripe().products.create({ name: "DocFast Pro", - description: "Unlimited PDF conversions via API. HTML, Markdown, and URL to PDF.", + description: "5,000 PDFs / month via API. HTML, Markdown, and URL to PDF.", }); productId = product.id; } diff --git a/package.json b/package.json index 7496134..58ea2e1 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Markdown/HTML to PDF API with built-in invoice templates", "main": "dist/index.js", "scripts": { + "build:html": "node scripts/build-html.cjs", "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", diff --git a/public/impressum.html b/public/impressum.html index b19697b..97968ad 100644 --- a/public/impressum.html +++ b/public/impressum.html @@ -20,38 +20,29 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Robo a { color: var(--accent); text-decoration: none; transition: color 0.2s; } a:hover { color: var(--accent-hover); } .container { max-width: 800px; margin: 0 auto; padding: 0 24px; } - -/* Nav */ nav { padding: 20px 0; border-bottom: 1px solid var(--border); } nav .container { display: flex; align-items: center; justify-content: space-between; } -.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; } +.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; text-decoration: none; } .logo span { color: var(--accent); } .nav-links { display: flex; gap: 28px; align-items: center; } .nav-links a { color: var(--muted); font-size: 0.9rem; font-weight: 500; } .nav-links a:hover { color: var(--fg); } - -/* Content */ -main { padding: 60px 0 80px; } -h1 { font-size: 2.5rem; font-weight: 800; margin-bottom: 16px; letter-spacing: -1px; } -h2 { font-size: 1.5rem; font-weight: 700; margin: 32px 0 16px; color: var(--accent); } -p { margin-bottom: 16px; line-height: 1.7; } -.highlight { background: rgba(52,211,153,0.08); border: 1px solid rgba(52,211,153,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: var(--accent); font-size: 0.9rem; } -.info { background: rgba(96,165,250,0.08); border: 1px solid rgba(96,165,250,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: #60a5fa; font-size: 0.9rem; } - -/* Footer */ -footer { padding: 40px 0; border-top: 1px solid var(--border); } -footer .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; } +.content { padding: 60px 0; min-height: 60vh; } +.content h1 { font-size: 2rem; font-weight: 800; margin-bottom: 32px; letter-spacing: -1px; } +.content h2 { font-size: 1.3rem; font-weight: 700; margin: 32px 0 16px; color: var(--fg); } +.content h3 { font-size: 1.1rem; font-weight: 600; margin: 24px 0 12px; color: var(--fg); } +.content p, .content li { color: var(--muted); margin-bottom: 12px; } +.content ul, .content ol { padding-left: 24px; } +.content strong { color: var(--fg); } +footer { padding: 32px 0; border-top: 1px solid var(--border); margin-top: 60px; } +footer .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; } .footer-left { color: var(--muted); font-size: 0.85rem; } -.footer-links { display: flex; gap: 24px; flex-wrap: wrap; } +.footer-links { display: flex; gap: 20px; flex-wrap: wrap; } .footer-links a { color: var(--muted); font-size: 0.85rem; } .footer-links a:hover { color: var(--fg); } - -/* Responsive */ -@media (max-width: 640px) { - main { padding: 40px 0 60px; } - h1 { font-size: 2rem; } - .footer-links { gap: 16px; } +@media (max-width: 768px) { footer .container { flex-direction: column; text-align: center; } + .nav-links { gap: 16px; } } @@ -114,4 +105,4 @@ footer .container { display: flex; align-items: center; justify-content: space-b - \ No newline at end of file + diff --git a/public/index.html b/public/index.html index 39d6af5..05458a3 100644 --- a/public/index.html +++ b/public/index.html @@ -17,7 +17,7 @@ - - - - - - - - -
-
-
🚀 Simple PDF API for Developers
-

HTML to PDF
in one API call

-

Convert HTML, Markdown, or URLs to pixel-perfect PDFs. Built-in templates for invoices & receipts. No headless browser headaches.

-
- - Read the Docs -
- -
-
-
- terminal -
-
-# Convert HTML to PDF — it's that simple -curl -X POST https://docfast.dev/v1/convert/html \ - -H "Authorization: Bearer YOUR_KEY" \ - -H "Content-Type: application/json" \ - -d '{"html": "<h1>Hello World</h1><p>Your first PDF</p>"}' \ - -o output.pdf -
-
-
-
- -
-
-
-
-
<1s
-
Avg. generation time
-
-
-
99.9%
-
Uptime SLA
-
-
-
HTTPS
-
Encrypted & secure
-
-
-
0 bytes
-
Data stored on disk
-
-
-
-
- -
-
-

Everything you need

-

A complete PDF generation API. No SDKs, no dependencies, no setup.

-
-
-
-

Sub-second Speed

-

Persistent browser pool — no cold starts. Your PDFs are ready before your spinner shows.

-
-
-
🎨
-

Pixel-perfect Output

-

Full CSS support including flexbox, grid, and custom fonts. Your brand, your PDFs.

-
-
-
📄
-

Built-in Templates

-

Invoice and receipt templates out of the box. Pass JSON data, get beautiful PDFs.

-
-
-
🔧
-

Dead-simple API

-

REST API. JSON in, PDF out. Works with curl, Python, Node, Go — anything with HTTP.

-
-
-
📐
-

Fully Configurable

-

A4, Letter, custom sizes. Portrait or landscape. Headers, footers, and margins.

-
-
-
🔒
-

Secure by Default

-

HTTPS only. Rate limiting. No data stored. PDFs stream directly — nothing touches disk.

-
-
-
-
- -
-
-

Simple, transparent pricing

-

Start free. Upgrade when you're ready. No surprise charges.

-
-
-
Free
-
$0 /mo
-
Perfect for side projects and testing
-
    -
  • 100 PDFs per month
  • -
  • All conversion endpoints
  • -
  • All templates included
  • -
  • Rate limiting: 10 req/min
  • -
- -
- -
-
-
- - - - - - - - - diff --git a/public/partials/_footer.html b/public/partials/_footer.html new file mode 100644 index 0000000..c0c1d4a --- /dev/null +++ b/public/partials/_footer.html @@ -0,0 +1,14 @@ + diff --git a/public/partials/_nav.html b/public/partials/_nav.html new file mode 100644 index 0000000..c1409ba --- /dev/null +++ b/public/partials/_nav.html @@ -0,0 +1,10 @@ + diff --git a/public/partials/_styles_base.html b/public/partials/_styles_base.html new file mode 100644 index 0000000..a03e308 --- /dev/null +++ b/public/partials/_styles_base.html @@ -0,0 +1,37 @@ + diff --git a/public/privacy.html b/public/privacy.html index 4974616..bdb66e0 100644 --- a/public/privacy.html +++ b/public/privacy.html @@ -20,41 +20,29 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Robo a { color: var(--accent); text-decoration: none; transition: color 0.2s; } a:hover { color: var(--accent-hover); } .container { max-width: 800px; margin: 0 auto; padding: 0 24px; } - -/* Nav */ nav { padding: 20px 0; border-bottom: 1px solid var(--border); } nav .container { display: flex; align-items: center; justify-content: space-between; } -.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; } +.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; text-decoration: none; } .logo span { color: var(--accent); } .nav-links { display: flex; gap: 28px; align-items: center; } .nav-links a { color: var(--muted); font-size: 0.9rem; font-weight: 500; } .nav-links a:hover { color: var(--fg); } - -/* Content */ -main { padding: 60px 0 80px; } -h1 { font-size: 2.5rem; font-weight: 800; margin-bottom: 16px; letter-spacing: -1px; } -h2 { font-size: 1.5rem; font-weight: 700; margin: 32px 0 16px; color: var(--accent); } -h3 { font-size: 1.2rem; font-weight: 600; margin: 24px 0 12px; } -p { margin-bottom: 16px; line-height: 1.7; } -ul { margin-bottom: 16px; padding-left: 24px; } -li { margin-bottom: 8px; line-height: 1.7; } -.highlight { background: rgba(52,211,153,0.08); border: 1px solid rgba(52,211,153,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: var(--accent); font-size: 0.9rem; } -.info { background: rgba(96,165,250,0.08); border: 1px solid rgba(96,165,250,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: #60a5fa; font-size: 0.9rem; } - -/* Footer */ -footer { padding: 40px 0; border-top: 1px solid var(--border); } -footer .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; } +.content { padding: 60px 0; min-height: 60vh; } +.content h1 { font-size: 2rem; font-weight: 800; margin-bottom: 32px; letter-spacing: -1px; } +.content h2 { font-size: 1.3rem; font-weight: 700; margin: 32px 0 16px; color: var(--fg); } +.content h3 { font-size: 1.1rem; font-weight: 600; margin: 24px 0 12px; color: var(--fg); } +.content p, .content li { color: var(--muted); margin-bottom: 12px; } +.content ul, .content ol { padding-left: 24px; } +.content strong { color: var(--fg); } +footer { padding: 32px 0; border-top: 1px solid var(--border); margin-top: 60px; } +footer .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; } .footer-left { color: var(--muted); font-size: 0.85rem; } -.footer-links { display: flex; gap: 24px; flex-wrap: wrap; } +.footer-links { display: flex; gap: 20px; flex-wrap: wrap; } .footer-links a { color: var(--muted); font-size: 0.85rem; } .footer-links a:hover { color: var(--fg); } - -/* Responsive */ -@media (max-width: 640px) { - main { padding: 40px 0 60px; } - h1 { font-size: 2rem; } - .footer-links { gap: 16px; } +@media (max-width: 768px) { footer .container { flex-direction: column; text-align: center; } + .nav-links { gap: 16px; } } @@ -200,4 +188,4 @@ footer .container { display: flex; align-items: center; justify-content: space-b - \ No newline at end of file + diff --git a/public/src/impressum.html b/public/src/impressum.html new file mode 100644 index 0000000..a9a3524 --- /dev/null +++ b/public/src/impressum.html @@ -0,0 +1,50 @@ + + + + + +Impressum — DocFast + + + + +{{> styles_base}} + + + +{{> nav}} + +
+
+

Impressum

+

Legal notice according to § 5 ECG and § 25 MedienG (Austrian law)

+ +

Company Information

+

Company: Cloonar Technologies GmbH

+

Address: Linzer Straße 192/1/2, 1140 Wien, Austria

+

Email: legal@docfast.dev

+ +

Legal Registration

+

Commercial Register: FN 631089y

+

Court: Handelsgericht Wien

+

VAT ID: ATU81280034

+

GLN: 9110036145697

+ +

Responsible for Content

+

Cloonar Technologies GmbH
+ Legal contact: legal@docfast.dev

+ +

Disclaimer

+

Despite careful content control, we assume no liability for the content of external links. The operators of the linked pages are solely responsible for their content.

+ +

The content of our website has been created with the greatest possible care. However, we cannot guarantee that the content is current, reliable or complete.

+ +

EU Online Dispute Resolution

+

Platform of the European Commission for Online Dispute Resolution (ODR): https://ec.europa.eu/consumers/odr

+
+
+ +{{> footer}} + + + diff --git a/public/src/privacy.html b/public/src/privacy.html new file mode 100644 index 0000000..ec66c42 --- /dev/null +++ b/public/src/privacy.html @@ -0,0 +1,133 @@ + + + + + +Privacy Policy — DocFast + + + + +{{> styles_base}} + + + +{{> nav}} + +
+
+

Privacy Policy

+

Last updated: February 16, 2026

+ +
+ This privacy policy is GDPR compliant and explains how we collect, use, and protect your personal data. +
+ +

1. Data Controller

+

Cloonar Technologies GmbH
+ Address: Vienna, Austria
+ Email: legal@docfast.dev
+ Data Protection Contact: privacy@docfast.dev

+ +

2. Data We Collect

+ +

2.1 Account Information

+ + +

2.2 API Usage Data

+ + +

2.3 Payment Information

+ + +
+ No PDF content stored: We process your HTML/Markdown input to generate PDFs, but do not store the content or resulting PDFs on our servers. +
+ +

3. Legal Basis for Processing

+ + +

4. Data Retention

+ + +

5. Third-Party Processors

+ +

5.1 Stripe (Payment Processing)

+

Purpose: Payment processing for Pro subscriptions
+ Data: Email, payment information
+ Location: EU (GDPR compliant)
+ Privacy Policy: https://stripe.com/privacy

+ +

5.2 Hetzner (Hosting)

+

Purpose: Server hosting and infrastructure
+ Data: All data processed by DocFast
+ Location: Germany (Nuremberg)
+ Privacy Policy: https://www.hetzner.com/legal/privacy-policy

+ +
+ EU Data Residency: All your data is processed and stored exclusively within the European Union. +
+ +

6. Your Rights Under GDPR

+ + +

To exercise your rights: Email privacy@docfast.dev

+ +

7. Cookies and Tracking

+

DocFast uses minimal technical cookies:

+ + +

8. Data Security

+ + +

9. International Transfers

+

Your personal data does not leave the European Union. Our infrastructure is hosted exclusively by Hetzner in Germany.

+ +

10. Contact for Data Protection

+

For questions about data processing or to exercise your rights:

+

Email: privacy@docfast.dev
+ Subject: Include "GDPR" in the subject line for priority handling

+ +

11. Changes to This Policy

+

We will notify users of material changes via email. Continued use of the service constitutes acceptance of updated terms.

+
+
+ +{{> footer}} + + + diff --git a/public/src/terms.html b/public/src/terms.html new file mode 100644 index 0000000..d9aeb6e --- /dev/null +++ b/public/src/terms.html @@ -0,0 +1,205 @@ + + + + + +Terms of Service — DocFast + + + + +{{> styles_base}} + + + +{{> nav}} + +
+
+

Terms of Service

+

Last updated: February 16, 2026

+ +
+ By using DocFast, you agree to these terms. Please read them carefully. +
+ +

1. Service Description

+

DocFast provides an API service for converting HTML, Markdown, and URLs to PDF documents. The service includes:

+ + +

2. Service Plans

+ +

2.1 Free Tier

+ + +

2.2 Pro Tier

+ + +
+ Overage: If you exceed your plan limits, API requests will return rate limiting errors. No automatic charges apply. +
+ +

3. Acceptable Use

+ +

3.1 Permitted Uses

+ + +

3.2 Prohibited Uses

+ + +
+ Violation consequences: Account termination, permanent ban, and legal action if necessary. +
+ +

4. API Key Security

+ + +

5. Service Availability

+ +

5.1 Uptime

+ + +

5.2 Performance

+ + +

6. Data Processing

+ + +

7. Payment Terms

+ +

7.1 Pro Subscription

+ + +

7.2 Cancellation

+ + +
+ EU Consumer Rights: 14-day right of withdrawal applies to digital services not yet delivered. Once you start using the Pro service, withdrawal right expires. +
+ +

8. Limitation of Liability

+ + +

9. Account Termination

+ +

9.1 By You

+ + +

9.2 By Us

+

We may terminate accounts for:

+ + +
+ Termination notice: We will provide reasonable notice except for immediate security threats. +
+ +

10. Intellectual Property

+ + +

11. Governing Law

+ + +

12. Changes to Terms

+

We may update these terms by:

+ + +

13. Contact Information

+

Questions about these terms:

+ + +
+ Effective Date: These terms are effective immediately upon posting. By using DocFast, you acknowledge reading and agreeing to these terms. +
+
+
+ +{{> footer}} + + + diff --git a/public/terms.html b/public/terms.html index f2f5802..3ade338 100644 --- a/public/terms.html +++ b/public/terms.html @@ -20,42 +20,29 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Robo a { color: var(--accent); text-decoration: none; transition: color 0.2s; } a:hover { color: var(--accent-hover); } .container { max-width: 800px; margin: 0 auto; padding: 0 24px; } - -/* Nav */ nav { padding: 20px 0; border-bottom: 1px solid var(--border); } nav .container { display: flex; align-items: center; justify-content: space-between; } -.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; } +.logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; text-decoration: none; } .logo span { color: var(--accent); } .nav-links { display: flex; gap: 28px; align-items: center; } .nav-links a { color: var(--muted); font-size: 0.9rem; font-weight: 500; } .nav-links a:hover { color: var(--fg); } - -/* Content */ -main { padding: 60px 0 80px; } -h1 { font-size: 2.5rem; font-weight: 800; margin-bottom: 16px; letter-spacing: -1px; } -h2 { font-size: 1.5rem; font-weight: 700; margin: 32px 0 16px; color: var(--accent); } -h3 { font-size: 1.2rem; font-weight: 600; margin: 24px 0 12px; } -p { margin-bottom: 16px; line-height: 1.7; } -ul { margin-bottom: 16px; padding-left: 24px; } -li { margin-bottom: 8px; line-height: 1.7; } -.highlight { background: rgba(52,211,153,0.08); border: 1px solid rgba(52,211,153,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: var(--accent); font-size: 0.9rem; } -.info { background: rgba(96,165,250,0.08); border: 1px solid rgba(96,165,250,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: #60a5fa; font-size: 0.9rem; } -.warning { background: rgba(251,191,36,0.08); border: 1px solid rgba(251,191,36,0.15); border-radius: 8px; padding: 16px; margin: 24px 0; color: #fbbf24; font-size: 0.9rem; } - -/* Footer */ -footer { padding: 40px 0; border-top: 1px solid var(--border); } -footer .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; } +.content { padding: 60px 0; min-height: 60vh; } +.content h1 { font-size: 2rem; font-weight: 800; margin-bottom: 32px; letter-spacing: -1px; } +.content h2 { font-size: 1.3rem; font-weight: 700; margin: 32px 0 16px; color: var(--fg); } +.content h3 { font-size: 1.1rem; font-weight: 600; margin: 24px 0 12px; color: var(--fg); } +.content p, .content li { color: var(--muted); margin-bottom: 12px; } +.content ul, .content ol { padding-left: 24px; } +.content strong { color: var(--fg); } +footer { padding: 32px 0; border-top: 1px solid var(--border); margin-top: 60px; } +footer .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; } .footer-left { color: var(--muted); font-size: 0.85rem; } -.footer-links { display: flex; gap: 24px; flex-wrap: wrap; } +.footer-links { display: flex; gap: 20px; flex-wrap: wrap; } .footer-links a { color: var(--muted); font-size: 0.85rem; } .footer-links a:hover { color: var(--fg); } - -/* Responsive */ -@media (max-width: 640px) { - main { padding: 40px 0 60px; } - h1 { font-size: 2rem; } - .footer-links { gap: 16px; } +@media (max-width: 768px) { footer .container { flex-direction: column; text-align: center; } + .nav-links { gap: 16px; } } @@ -273,4 +260,4 @@ footer .container { display: flex; align-items: center; justify-content: space-b - \ No newline at end of file + diff --git a/scripts/build-html.cjs b/scripts/build-html.cjs new file mode 100644 index 0000000..7b02b6d --- /dev/null +++ b/scripts/build-html.cjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * DocFast HTML Build Script + * Replaces {{> partial_name}} in source files with partial contents. + * Source: public/src/*.html → Output: public/*.html + * Partials: public/partials/_*.html + */ +const fs = require('fs'); +const path = require('path'); + +const srcDir = path.join(__dirname, '..', 'public', 'src'); +const outDir = path.join(__dirname, '..', 'public'); +const partialsDir = path.join(__dirname, '..', 'public', 'partials'); + +// Load all partials +const partials = {}; +if (fs.existsSync(partialsDir)) { + for (const f of fs.readdirSync(partialsDir)) { + if (f.startsWith('_') && f.endsWith('.html')) { + const name = f.replace(/^_/, '').replace(/\.html$/, ''); + partials[name] = fs.readFileSync(path.join(partialsDir, f), 'utf-8').trimEnd(); + } + } +} +console.log(`Loaded partials: ${Object.keys(partials).join(', ')}`); + +if (!fs.existsSync(srcDir)) { + console.error(`Source directory not found: ${srcDir}`); + process.exit(1); +} + +// Process each source file +const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.html')); +for (const file of files) { + let content = fs.readFileSync(path.join(srcDir, file), 'utf-8'); + + // Replace {{> partial_name}} with partial content + content = content.replace(/\{\{>\s*(\w+)\s*\}\}/g, (match, name) => { + if (partials[name]) return partials[name]; + console.warn(` Warning: partial '${name}' not found in ${file}`); + return match; + }); + + const outPath = path.join(outDir, file); + fs.writeFileSync(outPath, content); + console.log(` Built: ${file}`); +} +console.log('Done.');