# Generate PDFs in a Supabase Edge Function Generate PDFs in a Supabase Edge Function by sending HTML to a PDF API with fetch, then save the file to Supabase Storage. Supabase Edge Functions are a natural place for "generate the invoice when an order is paid" or "export this report as PDF". The hard part is rendering. Good HTML-to-PDF output needs a real browser engine, and edge functions can't run one. **Quick answer.** Store your key with `supabase secrets set HTML_PDF_KEY=...`, then `fetch` the hosted renderer from your function. This is the same request as curl: ```bash curl https://html-pdf.com/v1/pdf -H "Authorization: Bearer $HTML_PDF_KEY" \ -d '{"html": "

Invoice

", "format": "A4"}' -o invoice.pdf ``` ## Why you can't run Chromium in an Edge Function This section explains why the usual Puppeteer approach fails here. Edge Functions run on the Supabase Edge Runtime, a Deno-based isolate environment. - **No browser binary.** Puppeteer and Playwright need to launch a Chromium process. Isolates don't let you spawn processes or ship a ~100+ MB browser. - **Tight resource limits.** Functions have memory, CPU-time and bundle-size limits sized for short request handlers, not for a browser that renders pages. - **Pure-JS PDF libraries** (pdf-lib, jsPDF) do run, but they draw PDFs from coordinates. They don't lay out HTML and CSS, so you'd rebuild your template by hand. The practical answer is to keep the function as glue and send the HTML to a hosted Chromium renderer over HTTP. ## Set your API key as a secret This step makes your key available to the function without putting it in code. Get a key at https://html-pdf.com/login (100 free credits, and 1 credit = 1 PDF page), then run this. ```bash supabase secrets set HTML_PDF_KEY=your_key_here ``` For local development, put `HTML_PDF_KEY=...` in `supabase/functions/.env` and run `supabase functions serve`. Inside the function, read it with `Deno.env.get("HTML_PDF_KEY")`. ## Option 1: return the PDF directly This option streams the PDF straight back to the caller. Create the function with `supabase functions new render-pdf` and replace `index.ts`. ```ts // supabase/functions/render-pdf/index.ts Deno.serve(async (req) => { if (req.method !== "POST") { return new Response("Method not allowed", { status: 405 }); } const { html } = await req.json(); const res = await fetch("https://html-pdf.com/v1/pdf", { method: "POST", headers: { Authorization: `Bearer ${Deno.env.get("HTML_PDF_KEY")}`, "Content-Type": "application/json", }, body: JSON.stringify({ html, format: "A4", margin: "15mm", print_background: true }), }); if (!res.ok) { // Errors are JSON: { error: { code, message, hint } } return new Response(await res.text(), { status: res.status, headers: { "Content-Type": "application/json" }, }); } return new Response(res.body, { headers: { "Content-Type": "application/pdf", "Content-Disposition": 'attachment; filename="document.pdf"', }, }); }); ``` Passing `res.body` streams the PDF back without buffering it in memory. Deploy with `supabase functions deploy render-pdf`. Call it from your app with `supabase.functions.invoke("render-pdf", { body: { html } })` or a plain `fetch` that includes the user's JWT. ## Option 2: upload to Supabase Storage This option saves the PDF so you can link it to a row and serve it later. Create a bucket (for example a private `invoices` bucket) and upload from the function with supabase-js. ```ts // supabase/functions/invoice-pdf/index.ts import { createClient } from "npm:@supabase/supabase-js@2"; const supabase = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, ); Deno.serve(async (req) => { const { orderId, html } = await req.json(); const res = await fetch("https://html-pdf.com/v1/pdf", { method: "POST", headers: { Authorization: `Bearer ${Deno.env.get("HTML_PDF_KEY")}`, "Content-Type": "application/json", }, body: JSON.stringify({ html, format: "A4", prefer_css_page_size: true }), }); if (!res.ok) return new Response(await res.text(), { status: res.status }); const pdf = new Uint8Array(await res.arrayBuffer()); const path = `orders/${orderId}.pdf`; const { error } = await supabase.storage .from("invoices") .upload(path, pdf, { contentType: "application/pdf", upsert: true }); if (error) return new Response(error.message, { status: 500 }); const { data } = await supabase.storage .from("invoices") .createSignedUrl(path, 60 * 60); // 1 hour return Response.json({ path, signedUrl: data?.signedUrl, pages: res.headers.get("X-Pages"), }); }); ``` `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are available to deployed functions by default. The service role key bypasses Row Level Security. Keep this function server-side (check the caller's JWT or call it from a database webhook) and never return the key. If you don't need to keep the file, skip Storage and send `"response": "url"`. The API then returns JSON `{ "url": "...", "expires_at": "..." }` with a link valid for 24 hours. ## Building the HTML This section covers where the HTML comes from. Most projects keep an HTML template and fill it with row data from Postgres. Escape user-supplied values before you insert them, and use print CSS (`@page`, `break-inside: avoid`) for clean pages. A worked invoice template is at https://html-pdf.com/generate-invoice-pdf, and more starting points are in https://html-pdf.com/templates. You can also send `{"url": "https://..."}` to render a public page, such as a report route in your app, instead of HTML. ## Triggering it These are the common ways to run the function. - **From the client:** `supabase.functions.invoke(...)` after a user clicks "Download PDF". - **From the database:** a Database Webhook on `INSERT` into `orders` that calls the function. - **On a schedule:** `pg_cron` with `pg_net` to call the function for monthly statements. ## Limits and options This section lists the limits and the options you will reach for most. Requests can be up to 20 MB, each render has 30 seconds, and you're billed per page. The `X-Credits-Remaining` header shows your balance, and `402 insufficient_credits` means you're out. Useful options include `format`, `landscape`, `margin`, `header_html`/`footer_html` with page numbers, `wait_for_expression` for pages that render charts, and `max_pages` as a safety cap. The full reference is at https://html-pdf.com/docs. Other runtimes are covered in https://html-pdf.com/html-to-pdf-nodejs. ## FAQ ### Can I run Puppeteer in a Supabase Edge Function? No. Edge Functions run in Deno isolates that can't spawn a Chromium process or ship a ~100+ MB browser. Call a hosted renderer like `https://html-pdf.com/v1/pdf` with `fetch` instead. ### How do I save a generated PDF to Supabase Storage? Read the API response with `arrayBuffer()`, then upload it with `supabase.storage.from("invoices").upload(path, pdf, { contentType: "application/pdf" })`. Use `createSignedUrl` to share it from a private bucket. ### Can I get a download link instead of the PDF file? Yes. Send `"response": "url"` and the API returns JSON with a download link that lasts 24 hours. Your HTML is not stored. ### How much does PDF generation cost? One credit equals one PDF page. You get 100 free credits without a card, then packs cost $0.01 to $0.005 per page, purchased credits never expire, and failed renders cost nothing.