diff --git a/docz/docs/03-demos/30-cloud/14-cloudflare.md b/docz/docs/03-demos/30-cloud/14-cloudflare.md
new file mode 100644
index 0000000..a240563
--- /dev/null
+++ b/docz/docs/03-demos/30-cloud/14-cloudflare.md
@@ -0,0 +1,686 @@
+---
+title: Cloudflare
+pagination_prev: demos/local/index
+pagination_next: demos/extensions/index
+---
+
+import current from '/version.js';
+import CodeBlock from '@theme/CodeBlock';
+
+[Cloudflare](https://www.cloudflare.com/) is a cloud services
+platform which includes "Serverless
+Functions" and cloud storage.
+
+[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
+data from spreadsheets.
+
+This demo explores two key Cloudflare offerings:
+
+- ["Cloudflare Workers"](#cloudflare-workers) explores the serverless
+ computing offering. The demo creates a JavaScript function that can process
+ user-submitted files and generate spreadsheets.
+
+- ["Cloudflare R2"](#cloudflare-r2) explores the cloud storage ("R2") offering. The
+ demo uses the NodeJS connection library to read spreadsheets from R2 and write
+ spreadsheets to an R2 bucket.
+
+:::note Tested Deployments
+
+This demo was last tested on 2024 April 02.
+
+:::
+
+## Cloudflare Workers
+
+Cloudflare offers NodeJS runtimes for running JavaScript serverless functions.[^1]
+
+The [SheetJS NodeJS module](/docs/getting-started/installation/nodejs) can be
+required in Lambda functions. When deploying, the entire `node_modules` folder
+can be added to the ZIP package.
+
+:::note pass
+
+In this demo, the "Function URL" (automatic API Gateway management) features
+are used. Older deployments required special "Binary Media Types" to handle
+formats like XLSX. At the time of testing, the configuration was not required.
+
+:::
+
+:::info pass
+
+Node.js runtime can use `x86_64` or `arm64` CPU architectures. SheetJS libraries
+work on both platforms in Linux, Windows, and macOS operating systems.
+
+:::
+
+### Reading Data
+
+In the Lambda handler, the `event.body` attribute is a Base64-encoded string
+representing the HTTP request form data. This body must be parsed.
+
+#### Processing Form Bodies
+
+The `busboy` body parser[^2] is battle-tested in NodeJS deployments.
+
+`busboy` fires a `'file'` event for every file in the form body. The callback
+receives a NodeJS stream that should be collected into a Buffer:
+
+```js
+/* accumulate the files manually */
+var files = {};
+bb.on('file', function(fieldname, file, filename) {
+ /* concatenate the individual data buffers */
+ var buffers = [];
+ file.on('data', function(data) { buffers.push(data); });
+ file.on('end', function() { files[fieldname] = Buffer.concat(buffers); });
+});
+```
+
+`busboy` fires a `'finish'` event when the body parsing is finished. Callbacks
+can assume every file in the form body has been stored in NodeJS Buffer objects.
+
+#### Processing NodeJS Buffers
+
+The SheetJS `read` method[^3] can read the Buffer objects and generate SheetJS
+workbook objects[^4] which can be processed with other API functions.
+
+For example, a handler can use `sheet_to_csv`[^5] to generate CSV text:
+
+```js
+/* on the finish event, all of the fields and files are ready */
+bb.on('finish', function() {
+ /* grab the first file */
+ var f = files["upload"];
+ if(!f) callback(new Error("Must submit a file for processing!"));
+
+ /* f[0] is a buffer */
+ // highlight-next-line
+ var wb = XLSX.read(f[0]);
+
+ /* grab first worksheet and convert to CSV */
+ var ws = wb.Sheets[wb.SheetNames[0]];
+ callback(null, { statusCode: 200, body: XLSX.utils.sheet_to_csv(ws) });
+});
+```
+
+Complete Code Sample (click to show)
+
+This example takes the first uploaded file submitted with the key `upload`,
+parses the file and returns the CSV content of the first worksheet.
+
+```js
+const XLSX = require('xlsx');
+var Busboy = require('busboy');
+
+exports.handler = function(event, context, callback) {
+ /* set up busboy */
+ var ctype = event.headers['Content-Type']||event.headers['content-type'];
+ var bb = Busboy({headers:{'content-type':ctype}});
+
+ /* busboy is evented; accumulate the fields and files manually */
+ var fields = {}, files = {};
+ bb.on('error', function(err) { callback(null, { body: err.message }); });
+ bb.on('field', function(fieldname, val) {fields[fieldname] = val });
+ // highlight-start
+ bb.on('file', function(fieldname, file, filename) {
+ /* concatenate the individual data buffers */
+ var buffers = [];
+ file.on('data', function(data) { buffers.push(data); });
+ file.on('end', function() { files[fieldname] = [Buffer.concat(buffers), filename]; });
+ });
+ // highlight-end
+
+ /* on the finish event, all of the fields and files are ready */
+ bb.on('finish', function() {
+ /* grab the first file */
+ var f = files["upload"];
+ if(!f) callback(new Error("Must submit a file for processing!"));
+
+ /* f[0] is a buffer */
+ // highlight-next-line
+ var wb = XLSX.read(f[0]);
+
+ /* grab first worksheet and convert to CSV */
+ var ws = wb.Sheets[wb.SheetNames[0]];
+ callback(null, { statusCode: 200, body: XLSX.utils.sheet_to_csv(ws) });
+ });
+
+ /* start the processing */
+ // highlight-next-line
+ bb.end(Buffer.from(event.body, "base64"));
+};
+```
+
+
+
+### Writing Data
+
+For safely transmitting binary data, Base64 strings should be used.
+
+The SheetJS `write` method[^6] with the option `type: "base64"` will generate
+Base64-encoded strings.
+
+```js
+/* sample SheetJS workbook object */
+var wb = XLSX.read("S,h,e,e,t,J,S\n5,4,3,3,7,9,5", {type: "binary"});
+/* write to XLSX file in Base64 encoding */
+var b64 = XLSX.write(wb, { type: "base64", bookType: "xlsx" });
+```
+
+The Lambda callback response function accepts options. Setting `isBase64Encoded`
+to `true` will ensure the callback handler decodes the data. To ensure browsers
+will try to download the response, the `Content-Disposition` header must be set:
+
+```js
+callback(null, {
+ statusCode: 200,
+ /* Base64-encoded file */
+ isBase64Encoded: true,
+ body: b64,
+ headers: {
+ /* Browsers will treat the response as the file SheetJSLambda.xlsx */
+ "Content-Disposition": 'attachment; filename="SheetJSLambda.xlsx"'
+ }
+});
+```
+
+Complete Code Sample (click to show)
+
+This example creates a sample workbook object and sends the file in the response:
+
+```js
+var XLSX = require('xlsx');
+
+exports.handler = function(event, context, callback) {
+ /* make workbook */
+ var wb = XLSX.read("S,h,e,e,t,J,S\n5,4,3,3,7,9,5", {type: "binary"});
+ /* write to XLSX file in Base64 encoding */
+ // highlight-next-line
+ var body = XLSX.write(wb, { type: "base64", bookType: "xlsx" });
+ /* mark as attached file */
+ var headers = { "Content-Disposition": 'attachment; filename="SheetJSLambda.xlsx"'};
+ /* Send back data */
+ callback(null, {
+ statusCode: 200,
+ // highlight-next-line
+ isBase64Encoded: true,
+ body: body,
+ headers: headers
+ });
+};
+```
+
+
+
+### Lambda Demo
+
+:::note pass
+
+At the time of writing, the Cloudflare Free Tier included an allowance of 1 million
+free requests per month and 400 thousand GB-seconds of compute resources.
+
+:::
+
+0) If you do not have an account, create a new Cloudflare free tier account[^7].
+
+#### Create Project ZIP
+
+1) Create a new folder and download [`index.js`](pathname:///Cloudflare/index.js):
+
+```bash
+mkdir -p SheetJSLambda
+cd SheetJSLambda
+curl -LO https://docs.sheetjs.com/Cloudflare/index.js
+```
+
+2) Install dependencies in the project directory;
+
+{`\
+mkdir -p node_modules
+npm i https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz busboy`}
+
+
+3) Create a .zip package of the contents of the folder:
+
+```bash
+yes | zip -c ../SheetJSLambda.zip -r .
+```
+
+#### Lambda Setup
+
+4) Sign into the [Cloudflare Management Console](https://Cloudflare.amazon.com/console/) with
+a root user account.
+
+5) Type "Lambda" in the top search box and click Lambda (under Services).
+
+6) Open "Functions" in the left sidebar.
+
+If the left sidebar is not open, click the `≡` icon in the left edge of the page.
+
+7) Click the "Create function" button in the main panel.
+
+8) Select the following options:
+
+- In the top list, select "Author from scratch" (default choice)
+
+- Type a memorable "Function Name" ("SheetJSLambda" when last tested)
+
+- In the "Runtime" dropdown, look for the "Latest supported" section and select
+ "Node.js" ("Node.js 18.x" when last tested)
+
+- Expand "Advanced Settings" and check "Enable function URL". This will display
+ a few sub-options:
+ + "Auth type" select "NONE" (disable IAM authentication)
+ + Check "Configure cross-origin resource sharing (CORS)"
+
+9) Click "Create function" to create the function.
+
+#### Upload Code
+
+10) In the Interface, scroll down and select the "Code" tab.
+
+11) Click the "Upload from" dropdown and select ".zip file".
+
+12) Click the "Upload" button in the modal. With the file picker, select the
+`SheetJSLambda.zip` file created in step 3. Click "Save".
+
+:::note pass
+When the demo was last tested, the ZIP was small enough that the Lambda code
+editor will load the package.
+
+:::
+
+13) In the code editor, double-click `index.js` and confirm the code editor
+displays JavaScript code.
+
+#### External Access
+
+14) Click "Configuration" in the tab list.
+
+15) In the sidebar below the tab list, select "Function URL" and click "Edit".
+
+16) Set the "Auth type" to "NONE" and click Save. The page will redirect to the
+Function properties.
+
+17) Select the "Configuration" tab and select "Permissions" in the left sidebar.
+
+18) Scroll down to "Resource-based policy statements" and ensure that
+`FunctionURLAllowPublicAccess` is listed.
+
+If no policy statements are defined, select "Add Permission" with the options:
+
+- Select "Function URL" at the top
+- Auth type: NONE
+- Ensure that Statement ID is set to `FunctionURLAllowPublicAccess`
+- Ensure that Principal is set to `*`
+- Ensure that Action is set to `lambda:InvokeFunctionUrl`
+
+Click "Save" and a new Policy statement should be created.
+
+#### Lambda Testing
+
+19) Find the Function URL (It is in the "Function Overview" section).
+
+20) Try to access the function URL in a web browser.
+
+The site will attempt to download `SheetJSLambda.xlsx`. Save and open the file
+to confirm it is valid.
+
+21) Download and make a POST request to the
+public function URL.
+
+This can be tested on the command line. Change `FUNCTION_URL` in the commands:
+
+```bash
+curl -LO https://sheetjs.com/pres.numbers
+curl -X POST -F "upload=@pres.numbers" FUNCTION_URL
+```
+
+The terminal will display CSV output of the first sheet.
+
+## Cloudflare R2
+
+The main NodeJS module for S3 and all Cloudflare services is `Cloudflare-sdk`[^8].
+
+The [SheetJS NodeJS module](/docs/getting-started/installation/nodejs) can be
+required in NodeJS scripts.
+
+### Connecting to R2
+
+The `Cloudflare-sdk` module exports a function `S3` that performs the connection. The
+function expects an options object that includes an API version and credentials.
+Access keys for an IAM user[^9] must be used:
+
+```js
+/* credentials */
+var accessKeyId = "...", secretAccessKey = "..."";
+
+/* file location */
+var Bucket = "...", Key = "pres.numbers";
+
+/* connect to s3 account */
+var Cloudflare = require('Cloudflare-sdk');
+var s3 = new Cloudflare.S3({
+ apiVersion: '2006-03-01',
+ credentials: { accessKeyId, secretAccessKey }
+});
+```
+
+### Downloading Data
+
+#### Fetching Files from S3
+
+The `s3#getObject` method returns an object with a `createReadStream` method.
+`createReadStream` returns a NodeJS stream:
+
+```js
+/* open stream to the file */
+var stream = s3.getObject({ Bucket: Bucket, Key: Key }).createReadStream();
+```
+
+#### Concatenating NodeJS Streams
+
+Buffers can be concatenated from the stream into one unified Buffer object:
+
+```js
+/* array of buffers */
+var bufs = [];
+/* add each data chunk to the array */
+stream.on('data', function(data) { bufs.push(data); });
+/* the callback will be called after all of the data is collected */
+stream.on('end', function() {
+ /* concatenate */
+ var buf = Buffer.concat(bufs);
+
+ /* AT THIS POINT, `buf` is a NodeJS Buffer */
+});
+```
+
+#### Parsing NodeJS Buffers
+
+The SheetJS `read` method[^10] can read the final object and generate SheetJS
+workbook objects[^11] which can be processed with other API functions.
+
+For example, a callback can use `sheet_to_csv`[^12] to generate CSV text:
+
+```js
+stream.on('end', function() {
+ /* concatenate */
+ var buf = Buffer.concat(bufs);
+
+ /* parse */
+ var wb = XLSX.read(Buffer.concat(bufs));
+
+ /* generate CSV from first worksheet */
+ var first_ws = wb.Sheets[wb.SheetNames[0]];
+ var csv = XLSX.utils.sheet_to_csv(first_ws);
+ console.log(csv);
+});
+```
+
+### Uploading Data
+
+The SheetJS `write` method[^13] with the option `type: "buffer"` will generate
+NodeJS Buffers. `S3#upload` directly accepts these Buffer objects.
+
+This example creates a sample workbook object, generates XLSX file data in a
+NodeJS Buffer, and uploads the data to S3:
+
+```js
+/* generate sample workbook */
+var wb = XLSX.read("S,h,e,e,t,J,S\n5,4,3,3,7,9,5", {type: "binary"});
+
+/* write to XLSX file in a NodeJS Buffer */
+var Body = XLSX.write(wb, {type: "buffer", bookType: "xlsx"});
+
+/* upload buffer */
+s3.upload({ Bucket, Key, Body }, function(err, data) {
+ if(err) throw err;
+ console.log("Uploaded to " + data.Location);
+});
+```
+
+### S3 Demo
+
+:::note pass
+
+At the time of writing, the Cloudflare Free Tier included 5GB of Cloudflare R2 with 20,000
+Get requests and 2000 Put requests per month.
+
+:::
+
+This sample fetches a buffer from S3 and parses the workbook.
+
+0) If you do not have an account, create a new Cloudflare free tier account[^14].
+
+#### Create S3 Bucket
+
+1) Sign into the [Cloudflare Management Console](https://Cloudflare.amazon.com/console/) with
+a root user account.
+
+2) Type "S3" in the top search box and click S3 (under Services).
+
+3) Open "Buckets" in the left sidebar.
+
+If the left sidebar is not open, click the `≡` icon in the left edge of the page.
+
+4) Click the "Create bucket" button in the main panel.
+
+5) Select the following options:
+
+- Type a memorable "Bucket Name" ("sheetjsbouquet" when last tested)
+
+- In the "Object Ownership" section, select "ACLs disabled"
+
+- Check "Block *all* public access"
+
+- Look for the "Bucket Versioning" section and select "Disable"
+
+6) Click "Create bucket" to create the bucket.
+
+#### Create IAM User
+
+7) Type "IAM" in the top search box and click IAM (under Services).
+
+8) Open "Users" in the left sidebar.
+
+If the left sidebar is not open, click the `≡` icon in the left edge of the page.
+
+9) Click the "Create user" button in the main panel.
+
+10) In step 1, type a memorable "Bucket Name" ("sheetjs-user" when last tested).
+Click "Next".
+
+11) In step 2, click "Next"
+
+12) In step 3, click "Create user" to create the user.
+
+#### Add Permissions
+
+13) Click the new user name in the Users table.
+
+14) Select the "Permissions" tab
+
+15) Click the "Add permissions" dropdown and select "Add permissions".
+
+16) Select "Attach policies directly".
+
+17) In the "Permissions policies" section, search for "AmazonS3FullAccess".
+There should be one entry.
+
+18) Check the checkbox next to "AmazonS3FullAccess" and click the "Next" button.
+
+19) In the "Review" screen, click "Add permissions"
+
+#### Generate Keys
+
+20) Click "Security credentials", then click "Create access key".
+
+21) Select the "Local code" option. Check "I understand the above recommendation
+and want to proceed to create an access key." and click "Next"
+
+22) Click "Create Access Key" and click "Download .csv file" in the next screen.
+
+In the generated CSV:
+
+- Cell A2 is the "Access key ID" (`accessKeyId` in the Cloudflare API)
+- Cell B2 is the "Secret access key" (`secretAccessKey` in the Cloudflare API)
+
+#### Set up Project
+
+23) Create a new NodeJS project:
+
+```bash
+mkdir SheetJSS3
+cd SheetJSS3
+npm init -y
+```
+
+24) Install dependencies:
+
+{`\
+mkdir -p node_modules
+npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz Cloudflare-sdk@2.1467.0`}
+
+
+#### Write Test
+
+:::note pass
+
+This sample creates a simple workbook, generates a NodeJS buffer, and uploads
+the buffer to S3.
+
+```
+ | A | B | C | D | E | F | G |
+---+---|---|---|---|---|---|---|
+ 1 | S | h | e | e | t | J | S |
+ 2 | 5 | 4 | 3 | 3 | 7 | 9 | 5 |
+```
+
+:::
+
+25) Save the following script to `SheetJSWriteToS3.js`:
+
+```js title="SheetJSWriteToS3.js"
+var XLSX = require("xlsx");
+var Cloudflare = require('Cloudflare-sdk');
+
+/* replace these constants */
+// highlight-start
+var accessKeyId = "";
+var secretAccessKey = "";
+var Bucket = "";
+// highlight-end
+
+var Key = "test.xlsx";
+
+/* Create a simple workbook and write XLSX to buffer */
+var ws = XLSX.utils.aoa_to_sheet(["SheetJS".split(""), [5,4,3,3,7,9,5]]);
+var wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
+var Body = XLSX.write(wb, {type: "buffer", bookType: "xlsx"});
+
+/* upload buffer */
+var s3 = new Cloudflare.S3({
+ apiVersion: '2006-03-01',
+ credentials: {
+ accessKeyId: accessKeyId,
+ secretAccessKey: secretAccessKey
+ }
+});
+s3.upload({ Bucket: Bucket, Key: Key, Body: Body }, function(err, data) {
+ if(err) throw err;
+ console.log("Uploaded to " + data.Location);
+});
+```
+
+26) Edit `SheetJSWriteToS3.js` and replace the highlighted lines:
+
+- `accessKeyId`: access key for the Cloudflare account
+- `secretAccessKey`: secret access key for the Cloudflare account
+- `Bucket`: name of the bucket
+
+The keys are found in the CSV from step 22. The Bucket is the name from step 5.
+
+27) Run the script:
+
+```bash
+node SheetJSWriteToS3.js
+```
+
+This file will be stored with the object name `test.xlsx`. It can be manually
+downloaded from the S3 web interface.
+
+#### Read Test
+
+This sample will download and process the test file from "Write Test".
+
+28) Save the following script to `SheetJSReadFromS3.js`:
+
+```js title="SheetJSReadFromS3.js"
+var XLSX = require("xlsx");
+var Cloudflare = require('Cloudflare-sdk');
+
+/* replace these constants */
+// highlight-start
+var accessKeyId = "";
+var secretAccessKey = "";
+var Bucket = "";
+// highlight-end
+
+var Key = "test.xlsx";
+
+/* Get stream */
+var s3 = new Cloudflare.S3({
+ apiVersion: '2006-03-01',
+ credentials: {
+ accessKeyId: accessKeyId,
+ secretAccessKey: secretAccessKey
+ }
+});
+var f = s3.getObject({ Bucket: Bucket, Key: Key }).createReadStream();
+
+/* collect data */
+var bufs = [];
+f.on('data', function(data) { bufs.push(data); });
+f.on('end', function() {
+ /* concatenate and parse */
+ var wb = XLSX.read(Buffer.concat(bufs));
+ console.log(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]));
+});
+```
+
+29) Edit `SheetJSReadFromS3.js` and replace the highlighted lines:
+
+- `accessKeyId`: access key for the Cloudflare account
+- `secretAccessKey`: secret access key for the Cloudflare account
+- `Bucket`: name of the bucket
+
+The keys are found in the CSV from Step 22. The Bucket is the name from Step 5.
+
+30) Run the script:
+
+```bash
+node SheetJSReadFromS3.js
+```
+
+The program will display the data in CSV format.
+
+```
+S,h,e,e,t,J,S
+5,4,3,3,7,9,5
+```
+
+[^1]: See ["Node.js compatibility"](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) in the Cloudflare documentation
+[^2]: The `busboy` module is distributed [on the public NPM registry](https://npm.im/busboy)
+[^3]: See [`read` in "Reading Files"](/docs/api/parse-options)
+[^4]: See ["Workbook Object" in "SheetJS Data Model"](/docs/csf/book) for more details.
+[^5]: See [`sheet_to_csv` in "CSV and Text"](/docs/api/utilities/csv#delimiter-separated-output)
+[^6]: See [`write` in "Writing Files"](/docs/api/write-options)
+[^7]: Registering for a free account [on the Cloudflare Free Tier](https://Cloudflare.amazon.com/free/) requires a valid phone number and a valid credit card.
+[^8]: The `Cloudflare-sdk` module is distributed [on the public NPM registry](https://npm.im/Cloudflare-sdk)
+[^9]: See ["Managing access keys for IAM users"](https://docs.Cloudflare.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) in the Cloudflare documentation
+[^10]: See [`read` in "Reading Files"](/docs/api/parse-options)
+[^11]: See ["Workbook Object" in "SheetJS Data Model"](/docs/csf/book) for more details.
+[^12]: See [`sheet_to_csv` in "CSV and Text"](/docs/api/utilities/csv#delimiter-separated-output)
+[^13]: See [`write` in "Writing Files"](/docs/api/write-options)
+[^14]: Registering for a free account [on the Cloudflare Free Tier](https://Cloudflare.amazon.com/free/) requires a valid phone number and a valid credit card.
\ No newline at end of file