Skip to content
CastPDF

Generate PDFs with a Firebase Cloud Function

To generate a PDF in Firebase, write a 2nd gen Cloud Function with onCall that sends your Firestore data to CastPDF with fetch, saves the file to Cloud Storage through firebase-admin and returns a signed link. The key is a defineSecret parameter. The PDF generation API renders, so your function never bundles Chromium.

  • Updated October 2026
  • 100 free PDFs a month

PDF options for Firebase projects

Firebase apps usually create documents in Cloud Functions, because the client should never hold the data or the credentials needed to build them. 2nd gen functions run on Cloud Run, so a container can technically include a browser. Whether it should is another matter: memory, cold starts and deploy size all grow with it.

Ways to create PDFs from Firebase
ApproachWhere it runsWhat it costs you
Puppeteer inside a Cloud FunctionYour function, with Chromium downloaded at installA much larger deploy, far more memory than the default, and slow cold starts on every new instance
PDFKit or pdfmake in the functionYour function, pure JavaScriptLight and fast; you lay out pages in code and manage page breaks yourself
Client side generationThe user’s deviceInconsistent output across devices, and the data and logic sit in the client
CastPDFCalled from your functionOne HTTPS request per document; a plan above the free allowance

Puppeteer in a function tends to follow a familiar pattern: the first document after a quiet period is slow, memory has to be raised well above the default, and an upgrade to the Node runtime breaks the browser download. Calling an API turns the function back into a small piece of glue code that starts quickly on the default memory setting.

Set the secret and the parameters

Firebase stores secrets in Google Cloud Secret Manager. Set the CastPDF key with the CLI (it prompts for the value, so the key does not land in your shell history). Start with a test key beginning cpdf_test_. The template ID is not secret, so it can be a plain string parameter read from functions/.env. Make sure package.json in the functions folder targets Node 20 or later with "engines": { "node": "20" }.

Terminal
firebase functions:secrets:set CASTPDF_API_KEY

echo "BOOKING_TEMPLATE_ID=$BOOKING_TEMPLATE_ID" >> functions/.env

firebase deploy --only functions

A callable function that stores a booking confirmation

The example is a holiday rental app. When a guest taps “Download confirmation”, the app calls this function. It checks that the guest is signed in and owns the booking, renders the confirmation from a saved template, writes the PDF to the default Cloud Storage bucket and returns a signed URL that is valid for an hour.

functions/src/index.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https';
import { defineSecret, defineString } from 'firebase-functions/params';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { getStorage } from 'firebase-admin/storage';

initializeApp();

const castpdfKey = defineSecret('CASTPDF_API_KEY');
const bookingTemplateId = defineString('BOOKING_TEMPLATE_ID');

export const bookingConfirmationPdf = onCall(
  { secrets: [castpdfKey], timeoutSeconds: 120, memory: '256MiB', region: 'europe-west3' },
  async (request) => {
    if (!request.auth) {
      throw new HttpsError('unauthenticated', 'Sign in to download your confirmation.');
    }
    const bookingId = String(request.data.bookingId);
    const snap = await getFirestore().doc(`bookings/${bookingId}`).get();
    const booking = snap.data();
    if (!booking || booking.guestUid !== request.auth.uid) {
      throw new HttpsError('not-found', 'Booking not found.');
    }

    const res = await fetch('https://api.castpdf.com/v1/pdf', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${castpdfKey.value()}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': `booking-confirmation-${bookingId}-${booking.revision}`,
      },
      body: JSON.stringify({
        template_id: bookingTemplateId.value(),
        data: {
          booking: { reference: booking.reference, check_in: booking.checkIn, check_out: booking.checkOut, guests: booking.guests },
          property: booking.property,
          guest: { name: booking.guestName },
          charges: booking.charges,
        },
        filename: `confirmation-${booking.reference}`,
      }),
      signal: AbortSignal.timeout(90_000),
    });
    if (!res.ok) {
      const { error } = await res.json();
      console.error('CastPDF', res.status, error.code, error.message);
      const busy = res.status === 429 || res.status === 503;
      throw new HttpsError(busy ? 'unavailable' : 'internal', 'The confirmation could not be created.');
    }

    const file = getStorage().bucket().file(`confirmations/${request.auth.uid}/${booking.reference}.pdf`);
    await file.save(Buffer.from(await res.arrayBuffer()), { contentType: 'application/pdf', resumable: false });
    const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 60 * 60 * 1000 });
    return { path: file.name, url };
  },
);

Listing the secret in secrets: [castpdfKey] is what makes castpdfKey.value() work at runtime; without it the value is empty. timeoutSeconds: 120 lifts the function above the 60 second default, so a long document never gets cut off by the platform. HttpsError codes travel to the client SDK intact, which lets your app tell a busy moment (unavailable) apart from a real failure. Saving with resumable: false is the efficient choice for small files like this.

On the client, call it with httpsCallable(functions, 'bookingConfirmationPdf') from the firebase/functions package, pass { bookingId } and open result.data.url. The Firebase SDK attaches the user’s ID token for you, which is where request.auth comes from.

onRequest: stream the PDF straight back

Sometimes you want a plain URL that returns the file, for example a link in an admin tool. onRequest gives you an Express style request and response. Unlike onCall, it does not check Firebase Auth for you, so verify the ID token yourself before rendering anything.

functions/src/download.ts
import { onRequest } from 'firebase-functions/v2/https';
import { defineSecret, defineString } from 'firebase-functions/params';
import { getAuth } from 'firebase-admin/auth';

const castpdfKey = defineSecret('CASTPDF_API_KEY');
const bookingTemplateId = defineString('BOOKING_TEMPLATE_ID');

export const bookingConfirmationDownload = onRequest(
  { secrets: [castpdfKey], timeoutSeconds: 120 },
  async (req, res) => {
    const token = (req.get('Authorization') ?? '').replace('Bearer ', '');
    const user = await getAuth().verifyIdToken(token).catch(() => null);
    if (!user?.admin) {
      res.status(403).send('Forbidden');
      return;
    }

    const upstream = await fetch('https://api.castpdf.com/v1/pdf', {
      method: 'POST',
      headers: { Authorization: `Bearer ${castpdfKey.value()}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ template_id: bookingTemplateId.value(), data: req.body, filename: 'confirmation' }),
      signal: AbortSignal.timeout(90_000),
    });
    if (!upstream.ok) {
      res.status(502).send('Could not create the PDF');
      return;
    }
    res.set('Content-Type', 'application/pdf');
    res.set('Content-Disposition', 'inline; filename="confirmation.pdf"');
    res.send(Buffer.from(await upstream.arrayBuffer()));
  },
);

Here user.admin is a custom claim you set on staff accounts. Prefer onCall for anything your customers trigger: it handles auth, CORS and error mapping, and it pairs naturally with Cloud Storage. To create confirmations automatically when a booking is written, use onDocumentCreated from firebase-functions/v2/firestore and call the same rendering code.

The confirmation template and its JSON

Design the confirmation once in the dashboard: HTML and CSS with a live preview, and a Simple mode form for anyone who only needs to change the sample text. Your function then sends this kind of payload, built from the Firestore document:

Booking confirmation request
{
  "template_id": "5e7a9c1b-3d5f-4b8a-9c2e-4f6a8b0d2e3f",
  "data": {
    "booking": { "reference": "BK-77120", "check_in": "2026-12-20", "check_out": "2026-12-27", "guests": 4 },
    "property": { "name": "Larch Cabin", "address": "Glenridding, Cumbria", "host": "Fiona Bell" },
    "guest": { "name": "Daniel Okafor" },
    "charges": [
      { "label": "7 nights", "amount": 1260 },
      { "label": "Cleaning", "amount": 85 },
      { "label": "Security deposit (refundable)", "amount": 200 }
    ]
  },
  "filename": "confirmation-BK-77120"
}

Firestore timestamps are objects, not strings. Convert them with toDate().toISOString() before sending, then format them in the template with format_date. Amounts should be plain numbers so the money filter can format them for the guest’s currency.

Production checklist for Firebase

  • Keep the key in defineSecret only. Do not copy it into functions/.env, Remote Config or Firestore, all of which are easier to leak.
  • Set timeoutSeconds to at least 120. A render may take up to 30 seconds, and the client abort signal of 90 seconds should stay below the function timeout.
  • Grant the function’s service account the Service Account Token Creator role if getSignedUrl fails. Signing URLs needs that permission.
  • Enable App Check on callable functions to stop scripts outside your app from triggering renders on your account.
  • Run the function in a region close to your Firestore database to keep reads fast. CastPDF itself runs on servers in Germany, so a European region is a natural fit for EU apps.
  • Respect the per document limits of 50 pages and 40 MB. Use Cloud Storage lifecycle rules to delete old confirmations you no longer need.

Troubleshooting Firebase functions

Firebase errors you might meet and the fix
SymptomLikely causeFix
invalid_api_key (401) from CastPDFThe secret is not listed in the function options, so value() is emptyAdd it to secrets: [...] and redeploy
Permission iam.serviceAccounts.signBlob deniedThe runtime service account cannot sign URLsGrant it the Service Account Token Creator role
deadline-exceeded on the clientThe function timed out before the PDF arrivedRaise timeoutSeconds and keep the abort signal below it
template_render_error (422)A Firestore timestamp object was sent where the template expects a date stringConvert timestamps to ISO strings before sending
unauthenticated from the callableThe client called it before sign in completedWait for the auth state, then call the function
rate_limited (429) in the emulatorTest keys allow 20 requests per minutePause for the Retry-After seconds between bursts

The quickstart covers keys and starters, and the API reference documents each field used above. The Supabase page shows the same flow with an Edge Function and Supabase Storage, the AWS Lambda page uses S3, and the Node.js page explains the core calls outside any platform.

FAQ

Common questions

Should I use onCall or onRequest for PDF generation in Firebase?

Use onCall when your own app triggers the document, because it verifies the user and maps errors for you. Use onRequest when you need a plain URL that returns the file, and check the ID token yourself.

Can I run Puppeteer in a 2nd gen Cloud Function?

It is possible, since 2nd gen runs on Cloud Run, but you need much more memory and accept slow cold starts and a large deploy. Calling an API keeps the function small and quick to start.

Does generating PDFs work in the Firebase emulator?

Yes. The emulator runs your function locally and the fetch call reaches CastPDF as usual. Use a test key so development documents are free and watermarked.

Can I create the PDF automatically when a Firestore document is added?

Yes. Use onDocumentCreated from the v2 Firestore triggers, render the PDF and save it to Cloud Storage. Use an Idempotency-Key based on the document ID, because triggers can run more than once.

Which Node.js runtime should the function use?

Node 20 or later, which includes fetch and AbortSignal.timeout without extra packages. Set it in the engines field of the functions package.json file.

Where is the generated PDF stored?

In the example it goes to your own Cloud Storage bucket, under a path that includes the user ID. You control retention there. If a temporary link is enough, ask CastPDF for a url response instead.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.