{"id":47904854,"url":"https://github.com/alegerber/folio","last_synced_at":"2026-04-04T04:47:16.732Z","repository":{"id":349018166,"uuid":"1194842388","full_name":"alegerber/folio","owner":"alegerber","description":"Serverless-native PDF API. HTML → PDF on AWS Lambda, S3-first. TypeScript, Fastify, headless Chromium.","archived":false,"fork":false,"pushed_at":"2026-04-03T22:45:47.000Z","size":237,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T04:47:02.255Z","etag":null,"topics":["aws-lambda","html-to-pdf","pdf","pdf-generation","puppeteer","s3","serverless"],"latest_commit_sha":null,"homepage":"https://alegerber.github.io/folio/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alegerber.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-28T22:07:45.000Z","updated_at":"2026-04-03T19:33:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/alegerber/folio","commit_stats":null,"previous_names":["alegerber/folio"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/alegerber/folio","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alegerber%2Ffolio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alegerber%2Ffolio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alegerber%2Ffolio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alegerber%2Ffolio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alegerber","download_url":"https://codeload.github.com/alegerber/folio/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alegerber%2Ffolio/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31388168,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T04:26:24.776Z","status":"ssl_error","status_checked_at":"2026-04-04T04:23:34.147Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws-lambda","html-to-pdf","pdf","pdf-generation","puppeteer","s3","serverless"],"created_at":"2026-04-04T04:47:16.046Z","updated_at":"2026-04-04T04:47:16.727Z","avatar_url":"https://github.com/alegerber.png","language":"TypeScript","readme":"# folio\n\n[![License: MIT](https://img.shields.io/github/license/alegerber/folio)](LICENSE)\n\nServerless-native PDF API. HTML → PDF on AWS Lambda, S3-first. TypeScript, Fastify, headless Chromium.\n\nThe same container image runs on **AWS Lambda** or plain **Docker** without modification. PDF bytes go straight to S3 — no shared filesystem, no ephemeral `/tmp` issues.\n\nFolio is built for teams already on AWS who want a PDF service that fits their existing infrastructure.\n\n---\n\n## What folio includes\n\n- AWS Lambda and plain Docker support from the same container image\n- S3 upload plus presigned URL responses\n- API key authentication\n- HTML to PDF generation with CSS, headers, and footers\n- PDF merge, split, compress, and PDF/A routes\n- Prometheus-style metrics\n- Planned URL rendering, screenshots, OpenAPI docs, and document conversion\n\n---\n\n## Quick start\n\n```bash\n# Copy and edit environment variables\ncp .env.example .env\n\n# Optional: local SAM deploy config\ncp samconfig.example.toml samconfig.toml\n\n# Start API + MinIO (local S3)\ndocker compose up\n\n# Generate a PDF\ncurl -s -X POST http://localhost:8080/pdf/generate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"html\": \"\u003chtml\u003e\u003cbody\u003e\u003ch1\u003eHello\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e\"}' | jq .\n```\n\nThe MinIO console is at [http://localhost:9001](http://localhost:9001) (user: `minioadmin`, password: `minioadmin`).\n\nPresigned URLs in the response use `http://localhost:9000` and are directly openable from the host.\n\n---\n\n## Documentation\n\nThe GitHub Pages site is published from [`docs/`](docs/) and includes the full API reference.\n\n- [Documentation landing page](docs/index.html)\n- [API reference](docs/api/index.html)\n- [Contributing guide](CONTRIBUTING.md)\n- [GitHub Pages workflow](.github/workflows/pages.yml)\n\n## Container images\n\nPrebuilt images are published to GitHub Container Registry from semver tags like `v1.2.3`.\n\n```bash\ndocker pull ghcr.io/alegerber/folio:latest\ndocker pull ghcr.io/alegerber/folio:latest-full\n```\n\nEach release publishes `latest`, `x.y.z`, and `x.y` tags. Matching `-full` tags are also published so downstream deploys can adopt the long-lived tag shape now; they currently mirror the main image until the larger Docker-only conversion toolchain lands.\n\n## Local development\n\nPrerequisites: Docker and Docker Compose.\n\n```bash\n# Copy and edit environment variables\ncp .env.example .env\n\n# Start the API and a local MinIO (S3 replacement)\ndocker compose up\n\n# The API is now available at http://localhost:8080\ncurl -s -X POST http://localhost:8080/pdf/generate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"html\": \"\u003chtml\u003e\u003cbody\u003e\u003ch1\u003eHello\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e\"}' | jq .\n```\n\nThe MinIO console is available at [http://localhost:9001](http://localhost:9001) (user: `minioadmin`, password: `minioadmin`).\n\nPresigned URLs in the response use `http://localhost:9000` so they are directly openable from the host machine.\n\n## Development without Docker\n\n```bash\nnpm install\n\n# Requires real AWS credentials and S3_BUCKET set in your environment\nnpm run dev\n```\n\n## Authentication\n\nAll routes can be protected with a static API key passed in the `X-Api-Key` header.\n\n- Set `API_KEY` (minimum 32 characters) to enable authentication.\n- When `API_KEY` is not set, auth is skipped (useful for local dev).\n- Key comparison uses `crypto.timingSafeEqual` to prevent timing attacks.\n\n```bash\ncurl -X POST http://localhost:8080/pdf/generate \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Api-Key: your-secret-key-here\" \\\n  -d '{\"html\": \"\u003chtml\u003e\u003cbody\u003e\u003ch1\u003eHello\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e\"}'\n```\n\n---\n\n## Environment variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `S3_BUCKET` | yes | S3 bucket name |\n| `AWS_REGION` | yes | AWS region |\n| `AWS_ACCESS_KEY_ID` | prod only | IAM credentials (not needed with Lambda execution role) |\n| `AWS_SECRET_ACCESS_KEY` | prod only | IAM credentials |\n| `AWS_ENDPOINT_URL` | local only | Internal S3/MinIO endpoint — `http://minio:9000` |\n| `AWS_PUBLIC_ENDPOINT_URL` | local only | Public-facing endpoint for presigned URLs — `http://localhost:9000` |\n| `SIGNED_URL_EXPIRY_SECONDS` | no | Presigned URL TTL, default `3600` |\n| `LOG_LEVEL` | no | `trace` `debug` `info` `warn` `error` — default `info` |\n| `PORT` | no | HTTP port for local server, default `8080` |\n| `API_KEY` | recommended in prod | Static API key for request authentication (min 32 chars). Omit to disable auth. |\n| `GHOSTSCRIPT_PATH` | no | Path to the `gs` binary. Enables real image compression on `POST /pdf/compress` and activates the `POST /pdf/pdfa` route. |\n\nSee [.env.example](.env.example) for a ready-to-copy template.\n\n---\n\n## Development\n\n```bash\nnpm install\n\n# Run tests (no real browser or S3 — both are mocked)\nnpm test\n\n# Type check\nnpm run typecheck\n\n# Lint\nnpm run lint\n\n# Build CJS bundle to dist/\nnpm run build\n\n# Start with file watching (requires env vars)\nnpm run dev\n```\n\n## Scripts\n\n| Command | Description |\n|---|---|\n| `npm run dev` | Start with file watching |\n| `npm test` | Run all tests |\n| `npm run test:cov` | Tests with coverage |\n| `npm run typecheck` | TypeScript type check |\n| `npm run lint` | ESLint |\n| `npm run build` | Bundle with esbuild to `dist/` |\n\n---\n\n## Project structure\n\n```\nsrc/\n  server.ts              # Fastify app factory — shared by local + Lambda\n  local.ts               # Docker / plain Node entry point\n  lambda.ts              # Lambda handler — buildApp() at module level for browser reuse\n  config/env.ts          # Zod-parsed process.env — exits on missing required vars\n  plugins/\n    auth.ts              # API key authentication (X-Api-Key, timing-safe)\n    s3.ts                # s3 (upload) + s3Public (presigning) decorators\n    sensible.ts          # @fastify/sensible\n  routes/\n    health/              # GET /health\n    metrics/             # GET /metrics\n    pdf/                 # POST /pdf/generate, GET/DELETE /pdf/:id, POST /pdf/merge, /pdf/split, /pdf/compress, /pdf/pdfa\n  services/\n    pdf/PdfService.ts               # Puppeteer browser lifecycle + PDF generation\n    pdf/PdfOperationsService.ts     # split (pdf-lib), compress + PDF/A (Ghostscript / pdf-lib fallback)\n    storage/StorageService.ts       # S3 upload (s3) + presigned URL (s3Public)\n    metrics/MetricsService.ts       # In-memory Prometheus metrics (histograms + counters)\n  types/\n    index.ts                   # Shared TypeScript interfaces\n\ntest/integration/\n  generate.test.ts       # Full route tests via app.inject() — no real browser\n  metrics.test.ts        # Metrics endpoint after a generation request\n```\n\n---\n\n## Deployment\n\n### Lambda (default)\n\nDeploys as a container image to AWS Lambda via GitHub Actions.\n\n- **Every PR:** typecheck → lint → tests → Docker build check (parallel)\n- **Merge to `main`:** `sam build` → `sam deploy` → smoke test\n\nAuth uses GitHub Actions OIDC → AWS STS. Required secrets: `AWS_ACCOUNT_ID`, `ECR_REPOSITORY`, `S3_BUCKET_NAME`, `SAM_ARTIFACT_BUCKET`, `API_KEY`.\nThe SAM template takes the runtime API key as a `NoEcho` deployment parameter, and GitHub Actions passes it from the `API_KEY` repository secret.\n`./scripts/aws-setup.sh` bootstraps the OIDC provider, deploy role, buckets, ECR repository, and GitHub secrets.\n\nFor local SAM deploys, copy `samconfig.example.toml` to `samconfig.toml` and fill in the placeholders.\n\nIf deploys fail with `DELETE_FAILED`, the existing CloudFormation stack must be cleaned up before `sam deploy` can update it:\n\n```bash\naws cloudformation describe-stack-events --stack-name folio --region eu-central-1\naws cloudformation delete-stack --stack-name folio --region eu-central-1\n```\n\nIf deletion fails again, inspect the event log for the specific resource blocking cleanup.\n\n**Recommended Lambda configuration**\n\n| Setting | Value |\n|---|---|\n| Memory | 2048 MB |\n| Timeout | 120 s |\n| Architecture | x86_64 |\n| Package type | Image |\n\n### Docker / ECS / Fly.io / Railway\n\nBuild the image and run anywhere Docker is supported:\n\n```bash\ndocker build -t folio .\ndocker run -p 8080:8080 --env-file .env folio\n```\n\nPublished images are also available from GHCR:\n\n```bash\ndocker run -p 8080:8080 --env-file .env ghcr.io/alegerber/folio:latest\n```\n\n---\n\n## Roadmap\n\nSee [`.plans/PLAN.md`](.plans/PLAN.md) for the full feature roadmap.\n\nRelease automation and GHCR publishing are in place. Planned features (in order): URL rendering → Screenshot API → OpenAPI docs → LibreOffice conversion → Async webhooks → Queue-based scaling.\n\n---\n\n## Stack\n\n| Layer | Package | Version |\n|---|---|---|\n| Framework | Fastify | 5.x |\n| PDF rendering | puppeteer-core + @sparticuz/chromium | 24.x / 143.x |\n| PDF merging | pdf-lib | 1.x |\n| Validation | Zod | 4.x |\n| Storage | @aws-sdk/client-s3 | 3.x |\n| Logging | Pino | — |\n| Testing | Vitest | 4.x |\n| Build | esbuild | 0.27.x |\n| Runtime | Node.js | 24 |\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, Conventional Commits, PR expectations, and route patterns.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falegerber%2Ffolio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falegerber%2Ffolio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falegerber%2Ffolio/lists"}