Exports
Local Development & Testing with Cloudflare
When developing and testing Cloudflare Browser Rendering locally, you need to make your local application publicly accessible since Cloudflare's API must fetch the export routes over HTTPS. Use ngrok to create a secure tunnel:
Setup Steps
-
Start ngrok tunnel (replace
envoy.testwith your local domain):ngrok http 443 --host-header=envoy.test -
Update landlord domain with the ngrok URL:
php artisan landlord:domain https://f507d43eeff6.ngrok-free.app -
Set environment variables in your
.env:CLOUDFLARE_PUBLIC_BASE_URL=https://205c790b4f95.ngrok-free.app APP_URL=https://205c790b4f95.ngrok-free.app APP_ENV=staging -
Build production assets for optimal rendering:
npm run prod
Important Notes
- Use the same ngrok domain for both
CLOUDFLARE_PUBLIC_BASE_URLandAPP_URL - Set
APP_ENV=stagingto ensure proper asset compilation and caching - Production builds (
npm run prod) provide the most accurate rendering results - The ngrok tunnel must remain active during testing
- Cloudflare will automatically bypass ngrok interstitials because
CloudflareRenderServicesets thengrok-skip-browser-warning: 1header on all requests to prevent ngrok's browser warning page from interfering with rendering
// NB: THIS IS CRITICAL FOR LOCAL DEVELOPMENT WITH NGROK
$normalized['setExtraHTTPHeaders'] = ['ngrok-skip-browser-warning' => '1'];
Intro
If is often useful when certain app views, currently OPTTs and Elements, are visualised in locations outside of the Flowcode application such as board packs, OPAL decks, etc. The export functionality is built to allow for exporting of these app views to useful formats that can then be used in other places outside of the app itself.
The most popular export format is PDF, this is the only format offered for element exports, however image and print exports are also offered for OPTTs.
Export Considerations
When determining how to handle these exports the following key considerations were included:
- The front-end views are all built in
Vue JS, this was an important consideration in terms of the export technology - PDF is the most helpful export format as it can be imported or used in most external apps, can be searched, text-selected and is the most consistent in terms of layout
- Images and print versions are also required by the business requirements
Migration from Browsershot
Previously we used Browsershot (Puppeteer + local headless Chrome) for HTML-to-PDF/image conversion. In 2025, we migrated to Cloudflare's Browser Rendering API due to our managed hosting provider's security restrictions that prevent installing headless Chrome system-wide.
Cloudflare Browser Rendering
Cloudflare's Browser Rendering API is used for both PDF and image generation. Core parts:
App\Services\ExportService: Orchestrates export URL generation, selects PDF vs image, composes options, writes output tostorage/app/public/exports, and mirrors the legacy S3 flows (Element snapshots, Graphic archives).App\Services\Rendering\CloudflareRenderService: ImplementsRendererInterfaceto call Cloudflare endpoints forpdfandscreenshotwith normalized options.App\Jobs\GenerateExportJob: Runs exports asynchronously, applies global rate limiting and exponential backoff, and persists job status to cache for the modal UX.
Environment and configuration:
.env:CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKEN, optionalCLOUDFLARE_PUBLIC_BASE_URL.config/services.php→services.cloudflare.*(account_id, token, base_url, public_base_url).AppServiceProviderbindsRendererInterface→CloudflareRenderServiceand defines a named rate limiterexports.
Options mapping and rendering behavior:
- PDF:
printBackground,preferCSSPageSize,margin,landscapeare supported viapdfOptions. - Navigation:
gotoOptions.waitUntildefaults tonetworkidle0to allow Vue/Inertia hydration to complete;gotoOptions.timeoutdefaults to 30–60s depending on type. - Images:
screenshotOptions.type=png,fullPage=trueby default unless aselectoris provided (graphics often use#full-graphic-view). - Headers: an extra header is set to bypass certain interstitials (e.g., ngrok warnings) when applicable.
Rate limiting and retries:
- Free Cloudflare tokens allow around 6 requests per minute. We cap our internal queue throughput and back off aggressively on failures:
- Named limiter
exportsset to ~5/min. - Job backoff schedule: 15s, 30s, 60s, 120s, 300s.
- If the backlog grows or SLAs require faster turnaround, switch to a paid Cloudflare plan/token and increase the limiter accordingly.
- Named limiter
High-level flow remains the same as before; only the renderer changed from local Browsershot to remote Cloudflare.
Export Routes
All export views are ultimately exported using a dedicated route, eg:
Route::group(['prefix' => 'org/{organisation}/', 'as' => 'organisation.'], function () {
Route::group(['prefix' => 'export', 'as' => 'export.'], function () {
Route::get('/objectives', [ObjectiveExportController::class, 'index'])->name('objectives');
Route::get('/projects', [ProjectExportController::class, 'index'])->name('projects');
Route::get('/tasks', [TaskExportController::class, 'index'])->name('tasks');
Route::get('/targets', [TargetExportController::class, 'index'])->name('targets');
Route::get('/catalogues/{catalogue_module}/{catalogue}/elements/{element}/{tab}', [ElementExportController::class, 'index'])->name('element');
});
});
These views are responsible for building and displaying the assets that get generated by the renderer (Cloudflare currently), as well as the print view using the browser print functionality.
Protecting Export Routes
The export routes cannot be protected by authentication because they need to be accessible by the headless renderer. In order to protect them from unauthenticated access, an export_code is passed through the renderer request and checked in the ExportControllers:
//First check if the user is a member of the organisation
if (!$user || ($user && !$user->organisations()->exists($organisation->id))) {
// If not, check if it's a valid export request
if ($request->input('export_code') !== config('app.export_code')) {
return response()->json([
'message' => 'Invalid export code',
], Response::HTTP_UNAUTHORIZED);
}
}
Flow of Export Events
The following flow will use examples from the Element export process. The OPTT export process is very similar although the route, controller, method names etc will obviously be different.
1. User clicks export button
A get request is fired to the organisation.catalogue.element.export route with the relevant parameters.
2. The export modal is rendered (first modal state)
The corresponding controller method is accessed, being the modal method of ElementExportController. The export modal is rendered to users where they will be supplied with some context copy and a choice to either cancel (close modal) or proceed with export.
3. User clicks export button on modal (second modal state)
A post request is fired to the organisation.catalogue.element.export.run route with the relevant parameters.
The corresponding controller method is accessed, being the export method of ElementExportController.
The ExportService is called and handles the export process entirely using CloudflareRenderService under the hood. Exports run asynchronously via GenerateExportJob, which applies a global named limiter (exports) and a backoff schedule to cope with Cloudflare API limits (see Rate limiting below). The modal polls for status and updates when complete.
While the export is being generated, the state of the export modal changes to indicate to the user the export is in progress.
Note that the renderer ultimately fetches the export index view route. This route is responsible for rendering the actual export view that gets converted to PDF/image.
When the export is complete, including uploading to S3 bucket and persistence to the element_archives table of the database, the export modal state is changed one final time to indicate to the user that the export is complete and providing them a download button.
4. User clicks download button on modal (third modal state)
If the user clicks this download button the PDF is served to them in a new browser window.
5. User can access previous exports (element exports only)
If an element has a positive count for an export in the element_archives table, the user can access the previous exports by clicking the Snapshots tab on the front-end element view.
This will render a table with all previous exports and the user can click on the file name of an element export and they will be directed to the URL of that export in the S3 bucket.
Handling of Page Breaks
Element Exports
With Cloudflare, element PDFs rely on CSS-driven sizing rather than programmatically resizing the paper height. We enable preferCSSPageSize and printBackground, so the export views should set deterministic @page rules (size and margins) to achieve single-page outputs when desired. The legacy dynamic paper height approach from Browsershot is no longer used.
OPTT Exports
On the export views, page breaks are added to assist the PDF pagination. These are handled in the loops of the views, dependent on the perPage attribute.
The perPage attribute's value is set in the ExportController of each OPTT and varies depending on the layout (landscape or portrait) selected by the user.
<div
:class="{
'break-after-page ':
index % perPage === 0 && index !== 0,
}"
></div>
Export Layout Wrapper
For both Element and OPTT exports dedicated page wrapper and header components are used to wrap all exports and add a header to all exports which includes the Flowcode logo, Raizcorp logo and copyright message.
OPTT Export Notes
When handling an OPTT export, a form is used to retrieve the relevant details such as filters, layout attribute and type.
Building PDFs and images
Both PDFs and images are built and handled via ExportService using CloudflareRenderService which returns the URL (local or S3) to the frontend for the user to download.
Rate limiting and backoff (Cloudflare)
- Free Cloudflare tokens are limited to about 6 requests per minute.
- We throttle exports with a named limiter and rely on job backoff to smooth spikes:
- Backoff: 15s → 30s → 60s → 120s → 300s.
- If users experience long queues, consider provisioning a paid Cloudflare token and increasing the limiter.
Printing the OPTT view
Users can also print the OPTT view, however this isn't recommended as you will lose some formatting. This is handled separately by opening the OPTT Export view directly in a new tab and firing the browser print page automatically.