To securely serve files (like PDFs) from Cloudflare R2 to a React Native (Expo) app, a common pattern is: clients ask a secure backend for a short-lived URL, then use that URL to download directly. In our case, a Supabase Edge Function acts as the backend. It holds the R2 credentials (stored as environment secrets) and generates a presigned URL for each file. Since R2 is AWS-S3 compatible, we can use AWS Signature V4 libraries or helper packages to create these URLs.
- Store R2 credentials securely. Put your Cloudflare R2 Account ID, Access Key ID, Secret, and bucket name in Supabase Edge Function secrets (never embed them in the client).
- Use an S3-compatible client to sign. In the Edge Function (Node/Deno), instantiate an S3 client pointing to your R2 endpoint. For example, with the AWS JS SDK v3 or a lightweight library:
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const S3 = new S3Client({ region: "auto", // (R2 ignores region) endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: ACCESS_KEY_ID, secretAccessKey: SECRET_KEY } }); // Generate a presigned GET URL (1 hour expiry): const getUrl = await getSignedUrl(S3, new GetObjectCommand({ Bucket:"my-bucket", Key:"file.pdf" }), { expiresIn:3600 }); // Generate a presigned PUT URL with content-type restriction: const putUrl = await getSignedUrl(S3, new PutObjectCommand({ Bucket:"my-bucket", Key:"file.pdf", ContentType:"application/pdf" }), { expiresIn:600 });- This returns URLs like
https://my-bucket.<ACCOUNT>.r2.cloudflarestorage.com/file.pdf?...signature…. (Any change to these query parameters or missingContent-Typewill cause a 403 .) In your Edge Function, you would return the URL (or directly stream the upload). For example, one Supabase user created an Edge Function that verifies auth, then uses AWS SigV4 to upload to R2 and return the public URL. - Presigned vs direct upload. You can either return a presigned PUT URL to the client (client then
PUTthe file directly to R2), or have the Edge Function upload the file and simply return a public R2 link. Both use the same AWS-style signature logic. In either case, keep the expiry short (minutes rather than hours) and restrict the allowed Content-Type to avoid misuse. Also configure R2 bucket CORS to your app’s origin only so that only your app can use the URL.
Example (Edge Function flow):
- Client requests an upload token (or download link) from Edge Function, including its auth token.
- Edge Function verifies the user (e.g. via
supabase.auth.verify()), then calls R2. For upload, it can use AWS SDK or a library like [aws4fetch] or the S3 SDK to either generate a presigned PUT URL or directly send the file buffer to R2. For download, it generates a presigned GET URL. - Edge Function returns the URL to the client. The client then uses
fetch(or Expo FileSystem) to download or upload the file using that URL.
By keeping the signing step server-side, the R2 secret key never reaches the client. For reference, Cloudflare’s docs and community examples show this exact flow (using AWS SigV4 under the hood).
Expo FileSystem for PDF Downloads
On the React Native (Expo) side, you typically use expo-file-system (or the newer FileSystem API) to fetch the file from the presigned URL and store it locally. The common approach is downloadAsync() or File.downloadFileAsync():
import * as FileSystem from 'expo-file-system';
async function downloadPdf(url, name) {
const fileUri = FileSystem.documentDirectory + name;
const { uri } = await FileSystem.downloadAsync(url, fileUri);
console.log('Saved to', uri);
return uri;
}
This fetches the PDF at url and writes it to the app’s sandbox (e.g. /data/user/0/.../MyApp/cache/). Note:
- Save location: By default Expo writes to its app-specific directory (e.g.
FileSystem.documentDirectoryorcacheDirectory). These files are only accessible inside your app. (Users won’t see them in the device’s Downloads folder unless you explicitly export them.) - Android Permissions: Writing to external directories (like the public Downloads folder) is restricted. To save a PDF to Downloads on Android, use the
StorageAccessFramework: request a directory URI from the user and thenStorageAccessFramework.createFileAsync(dirUri, fileName, 'application/pdf'). (E.g. ask forWRITE_EXTERNAL_STORAGEviarequestDirectoryPermissionsAsync, then create and write the file.) - Media Library: The Expo MediaLibrary can save images/videos to the user gallery, but it does not support PDFs or arbitrary docs. Attempts to add a PDF to a MediaLibrary album will fail (StackOverflow reports a “could not create asset” error for PDFs).
- Getting the URI:
downloadAsync()returns a{ uri: <local path>, status }. Always checkstatus===200and catch errors. The URI is afile://...path in your app’s storage. You can then open or share this file via other Expo APIs (e.g.Sharing.shareAsync(uri)to let the user pick an app).
Common issues: Expo’s built-in downloader has limitations. On Android, large downloads (>60s) can time out (there’s a hard 60‑second limit). If your PDF is large or the network slow, the download may fail. Workarounds include using FileSystem.createDownloadResumable() with progress callbacks (so you can retry or show status) or breaking the download into chunks (not trivial with Expo). Also ensure you handle failures in a try/catch and delete any partially written file if an error occurs.
Displaying PDFs Securely in React Native
Once the PDF is downloaded, you need to display it. Expo Go does not include a native PDF viewer, so common approaches include:
- WebView: A practical workaround is to load the PDF in a
react-native-webview. For example, after downloading the file to a URI, you can point a WebView at it:<WebView source={{ uri: fileUri }} style={{ flex: 1 }} />or use a base64 data URL. This works entirely in Expo (no extra native modules). Bruno Pintos demonstrated an Expo-compatible PDF reader by combiningexpo-file-systemto fetch the file andreact-native-webviewto render it. - react-native-pdf (native): If you can use bare React Native (or a development client), the
react-native-pdflibrary provides an in-app PDF viewer component on both iOS and Android. It requires native linking, so it won’t run in plain Expo Go. - Open With External App: Use Expo’s
Sharing.shareAsync(uri)orIntentLauncher(Android) to ask the OS to open the PDF in an external viewer. This offloads rendering to another app. - Expo-Print / Expo-DocumentPicker: For some workflows, you can send the PDF to the print dialog (
expo-print), or allow the user to pick the file viaexpo-document-picker(though that’s for selecting, not viewing).
In summary, the simplest Expo-compatible solution is to download with FileSystem and display in a WebView or share to an external PDF app. This avoids ejecting while still giving the user a view of the document.
Performance Optimization & Error Handling
- Short-lived URLs: Keep presigned URLs validity very short (seconds or minutes), especially for sensitive PDFs. Treat the URL like a bearer token. After expiration, use a new signed URL.
- Content-Type Checks: Include the
Content-Typewhen signing PUT URLs (e.g."application/pdf") so that only matching uploads are accepted. On downloads, ensure the client sets appropriate headers (though GET won’t need special headers). - Progress & Timeouts: Expo’s downloader has no built-in progress UI, but you can use
DownloadResumablewith a callback to get progress updates. Be aware of the ~60s timeout on Android. For very large PDFs, consider using multiple ranged requests or instruct users to download only on Wi-Fi. - Retry Logic: Always wrap downloads in try/catch. On network errors or timeouts, retry or show an error to the user. Clean up incomplete files if a download fails.
- Native Modules: If performance is critical (e.g. very large PDFs or frequent downloads), consider ejecting and using
react-native-fsorrn-fetch-blob, which offer more control (pause/resume, background transfers) than Expo’s API.
Community Tools & Plugins
While there’s no Expo-specific “Cloudflare R2” package, several community tools can help:
aws4fetchor AWS SDK: As shown above, the AWS JS SDK or aws4fetch can be used inside Edge Functions or even React Native (with some polyfills).r2-presigned-url: A lightweight TypeScript library for generating R2 presigned URLs in Node/Workers. Example usage:import { generateR2PresignedUrl } from "r2-presigned-url"; const creds = { R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET: 'my-bucket' }; const url = await generateR2PresignedUrl( { key: "docs/manual.pdf", contentType: "application/pdf", expiresIn: 1800 }, creds );- This returns a presigned PUT URL you can use to upload, similar to the AWS SDK approach.
cloudflare-r2-edge: A Node/TypeScript client for R2 that can run in edge environments. It provides methods likeuploadFile,getObject, andsignedUrl().- Supabase SDK (Storage): If you store files in Supabase Storage (backed by AWS/S3) instead of raw R2, you can use
supabase.storage.from('bucket').createSignedUrl(...). However, as of early 2024 this does not work with a custom S3 endpoint like R2. This SDK is mainly for Supabase’s own storage backend with its CDN. - React Native PDF Libraries: For displaying PDFs, libraries like
react-native-file-viewerorrn-pdf-reader-jscan simplify rendering PDF after download. (These often require ejecting.) - Networking Libraries: If you eject,
react-native-fsorrn-fetch-blobgive finer control over downloads (background, multipart). In Expo Managed workflow,expo-file-systemis your only option.
Examples & References
- Supabase Edge + R2 Example: Vishal Sharma’s blog outlines using a Supabase Edge Function with AWS SigV4 to upload images to R2. He emphasizes keeping all R2 keys in Edge Function env variables (never the client). You can adapt his approach to PDFs (just change MIME type).
- Cloudflare Docs: The R2 documentation provides code for generating presigned GET/PUT URLs with AWS SDK v2/v3 and with
aws4fetch. These examples show exactly how to include headers and use the returned URL. - Expo Guides: The Expo FileSystem docs describe
downloadAsyncandcreateDownloadResumable(see Downloading files). The StorageAccessFramework docs show how to save files in Android’s Downloads folder (as used in). - Community Q&A: StackOverflow threads discuss Expo’s limitations and workarounds (e.g. saving non-image files, using
downloadAsyncin Expo). - Expo PDF Viewing: Several blogs and StackOverflow answers describe using WebViews to show PDFs in Expo without ejecting.
Summary: Use a Supabase Edge Function to generate a short-lived R2 presigned URL (GET or PUT), returning it to the app. In the React Native app, use expo-file-system to download the file from that URL into app storage, then display or open it (e.g. in a WebView). Secure the workflow by keeping credentials server-side, scoping the URL (content-type, CORS, expiration), and handling errors/retries on the client. The linked references provide code examples and best-practice tips for each step.
Sources: Official Cloudflare R2 docs, Supabase discussions, Expo documentation, and community examples