Coverfire recreates a configurable cover letter in HTML/CSS and generates print-ready PDF or plain-text output from the same content model.
The project currently has two parts:
- A Vite + React letter renderer
- A small Node + Puppeteer API with protected external routes and admin-only editing/generation routes
External API routes require the x-coverfire-key header. The header value must match COVERFIRE_API_KEY.
Returns the available body templates without exposing template body content.
Response:
[
{
"id": "standard",
"slug": "standard",
"name": "Standard"
}
]Generates a PDF cover letter.
Headers:
content-type: application/json
x-coverfire-key: replace-meRequest body:
{
"hiringManager": "Jane Doe",
"salutation": "Dear Jane,",
"title": "Senior Product Designer & Full-Stack Developer",
"role": "Senior Product Designer",
"company": "Acme",
"templateId": "standard"
}Rules:
hiringManageris optional and falls back toHiring Managersalutationis optional and overrides the template greeting when providedtitleis optional and falls back toSenior Product Designer & Full-Stack Developerroleis requiredcompanyis requiredtemplateIdis optional and selects a body template by idtemplateId: ""or a whitespace-onlytemplateIdresolves to the default body templateversionIdis accepted as a legacy alias whentemplateIdis not provided
Response:
200 OKContent-Type: application/pdfContent-Disposition: attachment; filename="...pdf"- Binary PDF body
Generates the same cover-letter content as plain text.
Headers and request body match POST /api/pdf.
Response:
200 OKContent-Type: text/plain; charset=utf-8Content-Disposition: inline; filename="...txt"- Plain-text body
Missing or invalid API key:
{
"error": "Unauthorized"
}Invalid request payload:
{
"error": "Invalid cover letter request",
"message": "role: Invalid input: expected string, received undefined",
"details": [
{
"path": "role",
"message": "Invalid input: expected string, received undefined"
}
]
}Copy .env.example and provide values as needed:
COVERFIRE_API_KEY: required shared secret forx-coverfire-keyADMIN_BASIC_AUTH_ENABLED: optional explicit Basic Auth toggle for admin routesADMIN_BASIC_AUTH_USERNAME: optional Basic Auth username for admin routesADMIN_BASIC_AUTH_PASSWORD: optional Basic Auth password for admin routesADMIN_BASIC_AUTH_REALM: optional Basic Auth realm, defaults toCoverfire AdminPORT: optional server port, defaults to3000COVERFIRE_RENDER_ORIGIN: optional render origin for local development when the Vite app is already runningPUPPETEER_EXECUTABLE_PATH: optional explicit Chrome/Chromium pathPUPPETEER_CACHE_DIR: optional explicit Puppeteer cache directoryCOVERFIRE_LOCAL_STORE_PATH: optional local JSON admin store path, defaults to.data/coverfire-admin.jsonUPSTASH_REDIS_REST_URL: Upstash Redis REST URL for persisted admin stateUPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token for persisted admin state
Run the Vite app and API server together:
COVERFIRE_API_KEY='replace-me' \
pnpm devRun only the API server against an already-running Vite app:
COVERFIRE_API_KEY='replace-me' \
COVERFIRE_RENDER_ORIGIN='http://127.0.0.1:5173' \
PUPPETEER_EXECUTABLE_PATH='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' \
pnpm startIf you are deploying to Render, use this build command so Chrome for Testing is installed into a project-local Puppeteer cache that is available at runtime:
pnpm install && pnpm run build:renderExample request:
curl -X POST http://127.0.0.1:3000/api/pdf \
-H 'content-type: application/json' \
-H 'x-coverfire-key: replace-me' \
--data '{
"hiringManager": "Jane Doe",
"role": "Senior Product Designer",
"company": "Acme",
"templateId": "standard"
}' \
--output cover-letter.pdfText example:
curl -X POST http://127.0.0.1:3000/api/text \
-H 'content-type: application/json' \
-H 'x-coverfire-key: replace-me' \
--data '{
"role": "Senior Product Designer",
"company": "Acme",
"templateId": ""
}'Admin routes are under /api/admin. They use Basic Auth when admin Basic Auth is enabled or when admin credentials are configured for non-local requests.
GET /api/adminPUT /api/adminGET /api/admin/logsGET /api/admin/logs/:idPOST /api/admin/logs/:id/regeneratePOST /api/admin/templatesGET /api/admin/templates/:idPUT /api/admin/templates/:idDELETE /api/admin/templates/:idPOST /api/admin/templates/:id/duplicatePOST /api/admin/templates/:id/defaultPOST /api/admin/pdfPOST /api/admin/text
Legacy aliases remain available:
POST /api/admin/generatemaps toPOST /api/admin/pdfPOST /api/admin/generate-textmaps toPOST /api/admin/text
GET /api/admin returns the full admin document:
{
"schemaVersion": 2,
"profile": {
"name": "Avana Vana",
"logoAlt": "Coverfire",
"addressLines": [],
"footerAddressLines": [],
"contacts": []
},
"defaults": {
"title": "Senior Product Designer & Full-Stack Developer",
"hiringManager": "Hiring Manager",
"defaultBodyTemplateId": "standard"
},
"bodyTemplates": [
{
"id": "standard",
"slug": "standard",
"name": "Standard",
"greeting": "Dear {{hiringManager}},",
"body": "Paragraph one.\n\nParagraph two for {{company}}.",
"signOff": "Warm regards,",
"isDefault": true,
"createdAt": "2026-06-21T00:00:00.000Z",
"updatedAt": "2026-06-21T00:00:00.000Z"
}
]
}PUT /api/admin accepts the same full document shape and returns the saved document.
Template create/update routes accept:
{
"name": "Template Name",
"slug": "template-slug",
"greeting": "Dear {{hiringManager}},",
"body": "Paragraph one.\n\nParagraph two for {{company}}.",
"signOff": "Warm regards,"
}Create, update, duplicate, and default routes return the saved body template:
{
"id": "template-id",
"slug": "template-slug",
"name": "Template Name",
"greeting": "Dear {{hiringManager}},",
"body": "Paragraph one.\n\nParagraph two for {{company}}.",
"signOff": "Warm regards,",
"isDefault": false,
"createdAt": "2026-06-21T00:00:00.000Z",
"updatedAt": "2026-06-21T00:00:00.000Z"
}DELETE /api/admin/templates/:id returns 204 No Content. The API rejects deleting the only body template.
POST /api/admin/pdf and POST /api/admin/text accept the same cover-letter request as the external generation routes, plus optional preview fields used by the admin UI:
{
"role": "Senior Product Designer",
"company": "Acme",
"templateId": "standard",
"previewBodyTemplateId": "standard",
"previewBodyTemplate": {
"name": "Draft Template",
"slug": "draft-template",
"greeting": "Dear {{hiringManager}},",
"body": "Draft body for {{company}}.",
"signOff": "Warm regards,"
}
}Admin PDF responses match /api/pdf. Admin text responses match /api/text.
Admin generation supports these optional headers:
X-Coverfire-Method: admin-ui | admin-previewX-Coverfire-Render-Origin: http://127.0.0.1:5173
GET /api/admin/logs returns generation log summaries:
[
{
"id": "log-entry-id",
"bodyTemplateId": "standard",
"bodyTemplateName": "Standard",
"company": "Acme",
"createdAt": "2026-06-21T00:00:00.000Z",
"filename": "avana_vana_senior_product_designer_cover_letter_acme_2026-21-06.pdf",
"hiringManager": "Jane Doe",
"method": {
"kind": "api",
"detail": "zapier"
},
"role": "Senior Product Designer",
"title": "Senior Product Designer & Full-Stack Developer"
}
]GET /api/admin/logs/:id returns the full log entry, including the request and admin document used for that generation. POST /api/admin/logs/:id/regenerate regenerates a PDF from that saved log entry and returns the same response shape as /api/admin/pdf.
If Upstash credentials are configured, admin state is stored in Redis. Otherwise the server persists admin state to a local JSON file for local development.
The current service can run as a single Node web service that:
- serves the built frontend from
dist - exposes the external API
- renders PDFs from the same deployed page
That makes it a good fit for lightweight Git-based platforms like Render.
For Render specifically:
- Build Command:
pnpm install && pnpm run build:render - Start Command:
pnpm start - Health Check Path:
/api/healthz - Optional Environment Variable:
PUPPETEER_CACHE_DIR=.cache/puppeteer