Turn HTML templates and dynamic data into pixel-perfect PDFs — in the browser or via API.
Repo: github.com/0xRahim/pdfCraft · Free & open source (MIT) · Self-host in minutes.
- Reusable HTML templates — author once with
{{variables}}, organize by status (active/draft/archived). - Live preview with sample data — sandboxed, script-free iframe; auto-filled editable samples per variable.
- XSS defense built in — templates scanned on save (no
<script>, event handlers, orjavascript:URLs); PDF renders run with JavaScript disabled and values HTML-escaped. - One-click PDF rendering — fill variables, generate, download a print-ready A4 PDF.
- Per-template API tokens — each integration gets its own revocable
pct_…token locked to one template.POSTdata → get a PDF download link. - Animated landing page — scroll-driven HTML → data → PDF story at
/, following the app's design system.
| Tool | Version | Notes |
|---|---|---|
| Bun | ≥ 1.x | Runs the API |
| Node.js | ≥ 22 | Runs the UI |
| Chromium / Chrome | any recent | Used by Puppeteer for PDF rendering (PUPPETEER_EXECUTABLE_PATH, falls back to /usr/bin/chromium) |
git clone https://github.com/0xRahim/pdfCraft.git
cd pdfCraft2. Run the API → http://localhost:3001
cd pdfcraft-api
bun install
cp .env.example .env # adjust secrets/paths as needed
bun run dev # hot-reload; `bun start` for plain run3. Run the UI → http://localhost:3000
cd pdfcraft-ui
npm install
echo 'VITE_API_BASE=http://localhost:3001' > .env.local
npm run devDemo credentials are seeded automatically and pre-filled on the login page:
- Email:
demo@pdfcraft.dev - Password:
Demo1234!
- Templates → New Template — paste HTML with placeholders like
<h1>Bill for {{customer}}</h1>, declarecustomer, amountas variables. - Preview tab — check the sandboxed render with auto-generated sample data.
- Render — fill the variables, hit Generate PDF, download the file.
- API button (per template card) — create a token and copy the curl example to integrate any app.
# 1. Create a token in the dashboard (API button on the template card),
# then generate a bill:
curl -X POST "http://localhost:3001/api/public/render/<templateId>" \
-H "Authorization: Bearer pct_••••••••" \
-H "Content-Type: application/json" \
-d '{"data": {"customer": "Acme Corp", "amount": "$1,240.00"}}'
# → { "downloadUrl": "/api/public/renders/<file>.pdf", "file": "<file>.pdf", "size": 41740 }
# 2. Download the finished PDF with the SAME token:
curl "http://localhost:3001/api/public/renders/<file>.pdf" \
-H "Authorization: Bearer pct_••••••••" -o bill.pdfBase URL defaults to http://localhost:3001. All errors are JSON ({ message }).
| Method & path | Description |
|---|---|
POST /api/auth/register |
{ email, password } → { token, user } |
POST /api/auth/login |
{ email, password } → { token, user } |
Session auth: Authorization: Bearer <jwt>.
| Method & path | Description |
|---|---|
GET /api/templates |
List templates (no HTML) |
GET /api/templates/:id |
Get template incl. HTML |
POST /api/templates |
Create (title, html, variables?, description?, status?); rejects JavaScript with 400 |
PUT /api/templates/:id |
Update (same validation) |
DELETE /api/templates/:id |
Delete + cascade its API tokens |
| Method & path | Description |
|---|---|
POST /api/templates/:id/tokens |
{ name } → token metadata plus plaintext secret (shown once) |
GET /api/templates/:id/tokens |
List metadata (prefix, created, last-used — never secrets) |
DELETE /api/templates/:id/tokens/:tokenId |
Revoke immediately |
| Method & path | Description |
|---|---|
POST /api/render/:id |
{ data } → { downloadUrl, file, size }; 400 lists missing variables |
GET /api/renders/:file |
Download the PDF binary |
| Method & path | Description |
|---|---|
POST /api/public/render/:templateId |
Same as JWT render; token must belong to the template (403 otherwise); only active templates |
GET /api/public/renders/:file |
Download; scoped to files rendered for the token's template |
Auth: Authorization: Bearer pct_…. Rate-limited (30 renders/min per token); revoked/unknown tokens → 401.
pdfCraft/
├── pdfcraft-api/ # Bun + Express + SQLite API (:3001)
│ └── src/
│ ├── index.ts # App wiring & route mounts
│ ├── auth.ts # JWT sessions + demo seed
│ ├── apiTokens.ts # pct_ token issue / hash / middleware
│ ├── sanitize.ts # No-JS policy: check + strip + escape
│ ├── browser.ts # Shared Puppeteer instance
│ ├── db.ts # bun:sqlite schema (users, templates, renders, api_tokens)
│ └── routes/
│ ├── auth.ts # /api/auth/*
│ ├── templates.ts # /api/templates/* incl. token management
│ ├── render.ts # /api/render/*, /api/renders/* (JWT)
│ └── publicRender.ts # /api/public/* (API token)
├── pdfcraft-ui/ # Vite + React 19 + Tailwind v4 UI (:3000)
│ ├── App.tsx # Route table (landing / auth / dashboard)
│ ├── app/
│ │ ├── landing/ # Animated public landing page
│ │ ├── dashboard/ # App shell, overview, templates CRUD
│ │ └── login|register/ # Auth pages
│ ├── components/
│ │ ├── landing/ # Navbar, hero, scroll pipeline, sections
│ │ ├── TemplatePreview.tsx # Sandboxed preview + sample-data editor
│ │ ├── TemplateRendererModal.tsx
│ │ └── ApiTokenModal.tsx # Token manager + integration docs
│ └── lib/
│ ├── api.ts # Typed API client
│ ├── authContext.tsx # Session state
│ └── router.tsx # Tiny history router
└── README.md / LICENSE
| Variable | Default | Description |
|---|---|---|
PORT |
3001 |
API listen port |
JWT_SECRET |
dev-only placeholder | Change in production — signs session tokens |
JWT_EXPIRES_IN |
7d |
Session lifetime |
DATABASE_URL |
./data/pdfcraft.db |
SQLite file (auto-created) |
RENDER_DIR |
./storage/renders |
Generated PDFs on disk |
DEMO_EMAIL / DEMO_PASSWORD |
demo@pdfcraft.dev / Demo1234! |
Seeded demo login |
CORS_ORIGINS |
http://localhost:3000 |
Allowed origins |
PUPPETEER_EXECUTABLE_PATH |
/usr/bin/chromium |
System browser; falls back to bundled Chrome |
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE |
http://localhost:3001 |
API base URL used by the client |
(GEMINI_API_KEY / APP_URL in .env.example only apply to AI-Studio hosting and are optional for self-hosting.)
- API secrets are SHA-256 hashed — plaintext exists only at creation; lists never expose it.
- Tokens are single-template scoped; cross-template render/download is rejected; revoked tokens fail closed.
- Defense in depth for XSS: reject-on-save validation (entity-decoding aware), render-time stripping, escaped variable substitution, sandboxed preview iframe, JS-disabled headless rendering.
- Known MVP limitation: templates (and their tokens) are workspace-global — any signed-in user can manage any template. Per-user ownership would need a
templates.userIdmigration.
# API
cd pdfcraft-api && bun run dev # hot-reload
bun start # run
bunx tsc --noEmit # typecheck
# UI
cd pdfcraft-ui && npm run dev # :3000
npm run build && npm run preview # production build
npx tsc --noEmit # typecheckIssues and pull requests are welcome at github.com/0xRahim/pdfCraft.
Please keep PRs focused, run both typechecks, and avoid committing secrets or local data/ / storage/ files.
MIT © 2026 0xRahim — see LICENSE. Free for commercial use.