
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Sat, 11 Apr 2026 01:41:36 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Building a scheduling system with Workers and Durable Objects]]></title>
            <link>https://blog.cloudflare.com/building-scheduling-system-with-workers-and-durable-objects/</link>
            <pubDate>Fri, 05 Aug 2022 15:45:57 GMT</pubDate>
            <description><![CDATA[ In this post we're going to show you how to build a scalable service that will schedule HTTP requests on a specific schedule or as one-off at a specific time  ]]></description>
            <content:encoded><![CDATA[ <p></p><p>We rely on technology to help us on a daily basis – if you are not good at keeping track of time, your calendar can remind you when it's time to prepare for your next meeting. If you made a reservation at a really nice restaurant, you don't want to miss it! You appreciate the app to remind you a day before your plans the next evening.</p><p>However, who tells the application when it's the right time to send you a notification? For this, we generally rely on scheduled events. And when you are relying on them, you really want to make sure that they occur. Turns out, this can get difficult. The scheduler and storage backend need to be designed with scale in mind - otherwise you may hit limitations quickly.</p><p>Workers, Durable Objects, and Alarms are actually a perfect match for this type of workload. Thanks to the distributed architecture of Durable Objects and their storage, they are a reliable and scalable option. Each Durable Object has access to its own isolated storage and alarm scheduler, both being automatically replicated and failover in case of failures.</p><p>There are many use cases where having a reliable scheduler can come in handy: running a webhook service, sending emails to your customers a week after they sign up to keep them engaged, sending invoices reminders, and more!</p><p>Today, we're going to show you how to build a scalable service that will schedule HTTP requests on a specific schedule or as one-off at a specific time as a way to guide you through any use case that requires scheduled events.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5QsLhy4QmfImRWULDDCpRx/07a879528b6632d850fda9c141ba8707/Scheduling-system-with-Workers-and-Durable-Objects---Diagram.png" />
            
            </figure>
    <div>
      <h3>Quick intro into the application stack</h3>
      <a href="#quick-intro-into-the-application-stack">
        
      </a>
    </div>
    <p>Before we dive in, here are some of the tools we’re going to be using today:</p><ul><li><p><a href="https://developers.cloudflare.com/workers/wrangler/get-started/#install">Wrangler</a> - CLI tool to develop and publish Workers</p></li><li><p><a href="https://developers.cloudflare.com/workers">Cloudflare Workers</a> - runtime</p></li><li><p><a href="https://developers.cloudflare.com/workers/learning/using-durable-objects/">Cloudflare Durable Objects</a> - storage for HTTP requests and <a href="/durable-objects-alarms/">Alarms</a> to schedule them</p></li></ul><p>The application is going to have the following components:</p><ul><li><p>Scheduling system API to accept scheduled requests and manage Durable Objects</p></li><li><p>Unique Durable Object per scheduled request, each with</p></li><li><p>Storage - keeping the request metadata, such as URL, body, or headers.</p></li><li><p>Alarm - a timer (trigger) to wake Durable Object up.</p></li></ul><p>While we will focus on building the application, the Cloudflare global network will take care of the rest – storing and replicating our data, and making sure to wake our Durable Objects up when the time's right. Let’s build it!</p>
    <div>
      <h3>Initialize new Workers project</h3>
      <a href="#initialize-new-workers-project">
        
      </a>
    </div>
    <p>Get started by generating a completely new Workers project using the <code>wrangler init</code> command, which makes creating new projects quick &amp; easy.</p><p><b>wrangler init -y durable-objects-requests-scheduler</b></p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6vgw0sXTG850ifbluBAbEy/57b6965270f884980aa64ebd4d90a914/pasted-image-0.png" />
            
            </figure><p>For more information on how to install, authenticate, or update Wrangler, check out <a href="https://developers.cloudflare.com/workers/wrangler/get-started">https://developers.cloudflare.com/workers/wrangler/get-started</a></p>
    <div>
      <h3>Preparing TypeScript types</h3>
      <a href="#preparing-typescript-types">
        
      </a>
    </div>
    <p>From my personal experience, at least a draft of TypeScript types significantly helps to be more productive down the road, so let's prepare and describe our scheduled request in advance. Create a file types.ts in src directory and paste the following TypeScript definitions.</p><p><b>src/types.ts</b></p>
            <pre><code>export interface Env {
    DO_REQUEST: DurableObjectNamespace
}
 
export interface ScheduledRequest {
  url: string // URL of the request
  triggerAt?: number // optional, unix timestamp in milliseconds, defaults to `new Date()`
  requestInit?: RequestInit // optional, includes method, headers, body
}</code></pre>
            
    <div>
      <h3>A scheduled request Durable Object class &amp; alarm</h3>
      <a href="#a-scheduled-request-durable-object-class-alarm">
        
      </a>
    </div>
    <p>Based on our architecture design, each scheduled request will be saved into its own Durable Object, effectively separating storage and alarms from each other and allowing our scheduling system to scale horizontally - there is no limit to the number of Durable Objects we create.</p><p>In the end, the Durable Object class is a matter of a couple of lines. The code snippet below accepts and saves the request body to a persistent storage and sets the alarm timer. Workers runtime will wake up the Durable Object and call the <code>alarm()</code> method to process the request.</p><p>The alarm method reads the scheduled request data from the storage, then processes the request, and in the end reschedules itself in case it's configured to be executed on a recurring schedule.</p><p><b>src/request-durable-object.ts</b></p>
            <pre><code>import { ScheduledRequest } from "./types";
 
export class RequestDurableObject {
  id: string|DurableObjectId
  storage: DurableObjectStorage
 
  constructor(state:DurableObjectState) {
    this.storage = state.storage
    this.id = state.id
  }
 
    async fetch(request:Request) {
    // read scheduled request from request body
    const scheduledRequest:ScheduledRequest = await request.json()
     
    // save scheduled request data to Durable Object storage, set the alarm, and return Durable Object id
    this.storage.put("request", scheduledRequest)
    this.storage.setAlarm(scheduledRequest.triggerAt || new Date())
    return new Response(JSON.stringify({
      id: this.id.toString()
    }), {
      headers: {
        "content-type": "application/json"
      }
    })
  }
 
  async alarm() {
    // read the scheduled request from Durable Object storage
    const scheduledRequest:ScheduledRequest|undefined = await this.storage.get("request")
 
    // call fetch on scheduled request URL with optional requestInit
    if (scheduledRequest) {
      await fetch(scheduledRequest.url, scheduledRequest.requestInit ? webhook.requestInit : undefined)
 
      // cleanup scheduled request once done
      this.storage.deleteAll()
    }
  }
}</code></pre>
            
    <div>
      <h3>Wrangler configuration</h3>
      <a href="#wrangler-configuration">
        
      </a>
    </div>
    <p>Once we have the Durable Object class, we need to create a Durable Object binding by instructing Wrangler where to find it and what the exported class name is.</p><p><b>wrangler.toml</b></p>
            <pre><code>name = "durable-objects-request-scheduler"
main = "src/index.ts"
compatibility_date = "2022-08-02"
 
# added Durable Objects configuration
[durable_objects]
bindings = [
  { name = "DO_REQUEST", class_name = "RequestDurableObject" },
]
 
[[migrations]]
tag = "v1"
new_classes = ["RequestDurableObject"]</code></pre>
            
    <div>
      <h3>Scheduling system API</h3>
      <a href="#scheduling-system-api">
        
      </a>
    </div>
    <p>The API Worker will accept POST HTTP methods only, and is expecting a JSON body with scheduled request data - what URL to call, optionally what method, headers, or body to send. Any other method than POST will return 405 - Method Not Allowed HTTP error.</p><p>HTTP <code>POST /:scheduledRequestId?</code> will create or override a scheduled request, where :scheduledRequestId is optional Durable Object ID returned from a scheduling system API before.</p><p><b>src/index.ts</b></p>
            <pre><code>import { Env } from "./types"
export { RequestDurableObject } from "./request-durable-object"
 
export default {
    async fetch(
        request: Request,
        env: Env
    ): Promise&lt;Response&gt; {
        if (request.method !== "POST") {
            return new Response("Method Not Allowed", {status: 405})
        }
 
        // parse the URL and get Durable Object ID from the URL
        const url = new URL(request.url)
        const idFromUrl = url.pathname.slice(1)
 
        // construct the Durable Object ID, use the ID from pathname or create a new unique id
        const doId = idFromUrl ? env.DO_REQUEST.idFromString(idFromUrl) : env.DO_REQUEST.newUniqueId()
 
        // get the Durable Object stub for our Durable Object instance
        const stub = env.DO_REQUEST.get(doId)
 
        // pass the request to Durable Object instance
        return stub.fetch(request)
    },
}</code></pre>
            <p>It's good to mention that the script above does not implement any listing of scheduled or processed webhooks. Depending on how the scheduling system would be integrated, you can save each created Durable Object ID to your existing backend, or write your own registry – using one of the <a href="https://developers.cloudflare.com/workers/get-started/storage-objects/">Workers storage options</a>.</p>
    <div>
      <h3>Starting a local development server and testing our application</h3>
      <a href="#starting-a-local-development-server-and-testing-our-application">
        
      </a>
    </div>
    <p>We are almost done! Before we publish our scheduler system to the Cloudflare edge, let's start Wrangler in a completely local mode to run a couple of tests against it and to see it in action – which will work even without an Internet connection!</p>
            <pre><code>wrangler dev --local</code></pre>
            
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2O2DcPjirepk7sgbtlMpbn/7cef2cc7d230dc4c0010ceb08c84da06/pasted-image-0--1-.png" />
            
            </figure><p>The development server is listening on localhost:8787, which we will use for scheduling our first request. The JSON request payload should match the TypeScript schema we defined in the beginning – required URL, and optional <code>triggerEverySeconds</code> number or <code>triggerAt</code> unix timestamp. When only the required URL is passed, the request will be dispatched right away.</p><p>An example of request payload that will send a GET request to <a href="https://example.com">https://example.com</a> every 30 seconds.</p>
            <pre><code>{
	"url":  "https://example.com",
	"triggerEverySeconds": 30,
}</code></pre>
            
            <pre><code>&gt; curl -X POST -d '{"url": "https://example.com", "triggerEverySeconds": 30}' http://localhost:8787
{"id":"000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f"}</code></pre>
            
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/74kGUzg7MYccSbLySud2e5/a7cdb2781a32c2b95953d25109f3bcce/pasted-image-0--2-.png" />
            
            </figure><p>From the wrangler logs we can see the scheduled request ID <code>000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f</code> is being triggered in 30s intervals.</p><p>Need to double the interval? No problem, just send a new POST request and pass the request ID as a pathname.</p>
            <pre><code>&gt; curl -X POST -d '{"url": "https://example.com", "triggerEverySeconds": 60}' http://localhost:8787/000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f
{"id":"000000018265a5ecaa5d3c0ab6a6997bf5638fdcb1a8364b269bd2169f022b0f"}</code></pre>
            <p>Every scheduled request gets a unique Durable Object ID with its own storage and alarm. As we demonstrated, the ID becomes handy when you need to change the settings of the scheduled request, or to deschedule them completely.</p>
    <div>
      <h3>Publishing to the network</h3>
      <a href="#publishing-to-the-network">
        
      </a>
    </div>
    <p>Following command will bundle our Workers application, export and bind Durable Objects, and deploy it to our workers.dev subdomain.</p>
            <pre><code>wrangler publish</code></pre>
            
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3p4tcXHgshwEhYihU8blKm/2dd1a3d76fcb72e056e94540228c2c3a/pasted-image-0--3-.png" />
            
            </figure><p>That's it, we are live! ? The URL of your deployment is shown in the Workers logs. In a reasonably short period of time we managed to write our own scheduling system that is ready to handle requests at scale.</p><p>You can check full source code in <a href="https://github.com/cloudflare/templates/tree/main/worker-example-request-scheduler">Workers templates repository</a>, or experiment from your browser without installing any dependencies locally using the <a href="https://workers.new/example-request-scheduler">StackBlitz template</a>.</p><p><b>What to see or build next</b></p><ul><li><p>New to Workers? Check our Get started guide.</p></li><li><p>Use <a href="https://www.cloudflare.com/products/zero-trust/access/">Access</a> or <a href="https://developers.cloudflare.com/workers/runtime-apis/service-bindings/">service bindings</a> if you want to protect your API from unauthorized access.</p></li><li><p>Got an idea for a Worker, get started in seconds =&gt; <a href="https://workers.new/typescript">https://workers.new/typescript</a> (<a href="https://workers.new/list">full list of StackBlitz supported templates</a>)</p></li><li><p>Dive into more Workers examples</p></li><li><p><a href="https://developers.cloudflare.com/workers/examples/">https://developers.cloudflare.com/workers/examples/</a></p></li><li><p><a href="https://github.com/cloudflare/templates">https://github.com/cloudflare/templates</a></p></li></ul> ]]></content:encoded>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Durable Objects]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[Serverless]]></category>
            <category><![CDATA[Edge]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">65UG8ITOXBu4pXT5pj8bSj</guid>
            <dc:creator>Adam Janiš</dc:creator>
        </item>
        <item>
            <title><![CDATA[Cloudflare and StackBlitz partner to deliver an instant and secure developer experience]]></title>
            <link>https://blog.cloudflare.com/cloudflare-stackblitz-partnership/</link>
            <pubDate>Mon, 09 May 2022 13:00:09 GMT</pubDate>
            <description><![CDATA[ We’re excited to announce that we’re partnering with StackBlitz. Together, we will bring the Wrangler experience closer to you – directly to your browser, with no dependencies required ]]></description>
            <content:encoded><![CDATA[ <p></p><p>We are starting our Platform Week focused on the most important aspect of a developer platform — developers. At the core of every announcement this week is developer experience. In other words, it doesn’t matter how groundbreaking the technology is if at the end of the day we’re not making your job as a developer easier.</p><p>Earlier today, we announced the <a href="/10-things-I-love-about-wrangler">general availability of a new Wrangler version</a>, making it easier than ever to get started and develop with Workers. We’re also excited to announce that we’re partnering with StackBlitz. Together, we will bring the Wrangler experience closer to you – directly to your browser, with no dependencies required!</p><p><a href="https://stackblitz.com/">StackBlitz</a> is a web-based code editor provided with a fresh and fast development environment on each page load. StackBlitz’s development environments are powered by WebContainers,  the first WebAssembly-based operating system, which boots secure development environments entirely within your browser tab.</p>
    <div>
      <h3>Introducing new Wrangler, running in your browser</h3>
      <a href="#introducing-new-wrangler-running-in-your-browser">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7me9i3OM1h9y2LH0SsjVZB/38ff6a52d4afe91b6d7b680353bc0e66/image2-6.png" />
            
            </figure><p>One of the <a href="/10-things-I-love-about-wrangler">Wrangler improvements we announced</a> today is the option to easily run Wrangler in any Node.js environment, including your browser which is now powered by WebContainers!</p><p>StackBlitz’s WebContainers are optimized for starting any project within seconds, including the installation of all dependencies. Whenever you’re ready to start a fresh development environment, you can refresh the browser tab running StackBlitz’s editor and have everything instantly ready to go.</p><p>Don’t just take our word for it, you can test this out yourself by opening up a sample project on <a href="https://workers.new/typescript">https://workers.new/typescript</a>.Note: currently, only Chromium based browsers are supported.</p><p>You can think of WebContainers as an in-browser operating system: they include features like a file system, multi-process and multi-threading application support, and a virtualized TCP network stack with the use of ServiceWorkers.</p><p>Interested in learning more about WebContainers? Check out the <a href="https://blog.stackblitz.com/posts/introducing-webcontainers/">introduction blog post</a> or WebContainer working group <a href="https://github.com/stackblitz/webcontainer-core#-webcontainer-wg">GitHub repository</a>.</p>
    <div>
      <h3>Powering a better developer experience and documentation</h3>
      <a href="#powering-a-better-developer-experience-and-documentation">
        
      </a>
    </div>
    <div></div>
<p></p><p>We’re excited about all the possibilities that instant development environments running in the browser open us up to. For example, they enable us to embed or link full code projects directly from our documentation examples and tutorials without waiting for a remote server to spin up a container with your environment.</p><p>Try out the following templates and have a little sneak peek of the developer experience we are working together to enable, as running a new Workers application locally was never easier!</p><p><a href="https://workers.new/router">https://workers.new/router</a><a href="https://workers.new/durable-objects">https://workers.new/durable-objects</a><a href="https://workers.new/typescript">https://workers.new/typescript</a></p>
    <div>
      <h3>What’s next</h3>
      <a href="#whats-next">
        
      </a>
    </div>
    <p>StackBlitz supports running Wrangler in a local mode today, and we are working together to enable features that require authentication to bring the full developer lifecycle inside your browser - including development on the edge, publishing, and debugging or tailing logs of your published Workers.</p><p>Share what you have built with us and stay tuned for more updates! Make sure to follow us on <a href="https://twitter.com/cloudflaredev">Twitter</a> or join <a href="https://discord.gg/cloudflaredev">our Discord</a> Developers Community server.</p> ]]></content:encoded>
            <category><![CDATA[Platform Week]]></category>
            <category><![CDATA[Partners]]></category>
            <category><![CDATA[Product News]]></category>
            <guid isPermaLink="false">rR8VK0FSQMzSCKxy19cXx</guid>
            <dc:creator>Adam Janiš</dc:creator>
            <dc:creator>Tomek Sułkowski</dc:creator>
        </item>
        <item>
            <title><![CDATA[Build your next video application on Cloudflare]]></title>
            <link>https://blog.cloudflare.com/build-video-applications-cloudflare/</link>
            <pubDate>Fri, 19 Nov 2021 13:59:21 GMT</pubDate>
            <description><![CDATA[ Today, we’re going to build a video application inspired by Cloudflare TV. We’ll have user authentication and the ability for administrators to upload recorded videos or livestream new content. Think about being able to build your own YouTube or Twitch using Cloudflare services! ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Historically, building video applications has been very difficult. There's a lot of complicated tech behind recording, encoding, and playing videos. Luckily, <a href="https://www.cloudflare.com/products/cloudflare-stream/">Cloudflare Stream</a> abstracts all the difficult parts away, so you can build custom video and streaming applications easily. Let's look at how we can combine Cloudflare Stream, Access, Pages, and Workers to create a high-performance video application with very little code.</p><p>Today, we’re going to build a video application inspired by <a href="https://cloudflare.tv/live">Cloudflare TV</a>. We’ll have user authentication and the ability for administrators to upload recorded videos or livestream new content. Think about being able to build your own YouTube or Twitch using Cloudflare services!</p><div></div>
    <div>
      <h3>Fetching a list of videos</h3>
      <a href="#fetching-a-list-of-videos">
        
      </a>
    </div>
    <p>On the main page of our application, we want to display a list of all videos. The videos are uploaded and stored with Cloudflare Stream, but more on that later! This code could be changed to display only the "trending" videos or a selection of videos chosen for each user. For now, we'll use the search API and pass in an empty string to return all.</p>
            <pre><code>import { getSignedStreamId } from "../../src/cfStream"

export async function onRequestGet(context) {
    const {
        request,
        env,
        params,
    } = context

    const { id } = params

    if (id) {
        const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/stream/${id}`, {
            method: "GET",
            headers: {
                "Authorization": `Bearer ${env.CF_API_TOKEN_STREAM}`
            }
        })

        const video = (await res.json()).result

        if (video.meta.visibility !== "public") {
            return new Response(null, {status: 401})
        }

        const signedId = await getSignedStreamId(id, env.CF_STREAM_SIGNING_KEY)

        return new Response(JSON.stringify({
            signedId: `${signedId}`
        }), {
            headers: {
                "content-type": "application/json"
            }
        })
    } else {
        const url = new URL(request.url)
        const res = await (await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/stream?search=${url.searchParams.get("search") || ""}`, {
            headers: {
                "Authorization": `Bearer ${env.CF_API_TOKEN_STREAM}`
            }
        })).json()

        const filteredVideos = res.result.filter(x =&gt; x.meta.visibility === "public") 
        const videos = await Promise.all(filteredVideos.map(async x =&gt; {
            const signedId = await getSignedStreamId(x.uid, env.CF_STREAM_SIGNING_KEY)
            return {
                uid: x.uid,
                status: x.status,
                thumbnail: `https://videodelivery.net/${signedId}/thumbnails/thumbnail.jpg`,
                meta: {
                    name: x.meta.name
                },
                created: x.created,
                modified: x.modified,
                duration: x.duration,
            }
        }))
        return new Response(JSON.stringify(videos), {headers: {"content-type": "application/json"}})
    }
}</code></pre>
            <p>We'll go through each video, filter out any private videos, and pull out the metadata we need, such as the thumbnail URL, ID, and created date.</p>
    <div>
      <h3>Playing the videos</h3>
      <a href="#playing-the-videos">
        
      </a>
    </div>
    <p>To allow users to play videos from your application, they need to be public, or you'll have to sign each request. Marking your videos as public makes this process easier. However, there are many reasons you might want to control access to your videos. If you want users to log in before they play them or the ability to limit access in any way, mark them as private and use signed URLs to control access. You can find more information about securing your videos <a href="https://developers.cloudflare.com/stream/viewing-videos/securing-your-stream">here</a>.</p><p>If you are testing your application locally or expect to have fewer than 10,000 requests per day, you can call the /token endpoint to generate a signed token. If you expect more than 10,000 requests per day, sign your own tokens as we do here using <a href="https://jwt.io/">JSON Web Tokens</a>.</p>
    <div>
      <h3>Allowing users to upload videos</h3>
      <a href="#allowing-users-to-upload-videos">
        
      </a>
    </div>
    <p>The next step is to build out an admin page where users can upload their videos. You can find documentation on allowing user uploads <a href="https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads">here</a>.</p><p>This process is made easy with the Cloudflare Stream API. You use your <a href="https://developers.cloudflare.com/api/tokens/create">API token</a> and account ID to generate a unique, one-time upload URL. Just make sure your token has the Stream:Edit permission. We hook into all POST requests from our application and return the generated upload URL.</p>
            <pre><code>export const cfTeamsAccessAuthMiddleware = async ({request, data, env, next}) =&gt; {
    try {
        const userEmail = request.headers.get("cf-access-authenticated-user-email")

        if (!userEmail) {
            throw new Error("User not found, make sure application is behind Cloudflare Access")
        }
  
        // Pass user info to next handlers
        data.user = {
            email: userEmail
        }
  
        return next()
    } catch (e) {
        return new Response(e.toString(), {status: 401})
    }
}

export const onRequest = [
    cfTeamsAccessAuthMiddleware
]</code></pre>
            <p>The admin page contains a form allowing users to drag and drop or upload videos from their computers. When a logged-in user hits submit on the upload form, the application generates a unique URL and then posts the <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">FormData</a> to it. This code would work well for building a video sharing site or with any application that allows user-generated content.</p>
            <pre><code>async function getOneTimeUploadUrl() {
    const res = await fetch('/api/admin/videos', {method: 'POST', headers: {'accept': 'application/json'}})
    const upload = await res.json()
    return upload.uploadURL
}

async function uploadVideo() {
    const videoInput = document.getElementById("video");

    const oneTimeUploadUrl = await getOneTimeUploadUrl();
    const video = videoInput.files[0];
    const formData = new FormData();
    formData.append("file", video);

    const uploadResult = await fetch(oneTimeUploadUrl, {
        method: "POST",
        body: formData,
    })
}</code></pre>
            
    <div>
      <h3>Adding real time video with Stream Live</h3>
      <a href="#adding-real-time-video-with-stream-live">
        
      </a>
    </div>
    <p>You can add a <a href="https://www.cloudflare.com/developer-platform/solutions/live-streaming/">livestreaming</a> section to your application as well, using <a href="/stream-live/">Stream Live</a> in conjunction with the techniques we've already covered.  You could allow logged-in users to start a broadcast and then allow other logged-in users, or even the public, to watch it in real-time! The streams will automatically save to your account, so they can be viewed immediately after the broadcast finishes in the main section of your application.</p>
    <div>
      <h3>Securing our app with middleware</h3>
      <a href="#securing-our-app-with-middleware">
        
      </a>
    </div>
    <p>We put all authenticated pages behind this middleware function. It checks the request headers to make sure the user is sending a valid authenticated user email.</p>
            <pre><code>export const cfTeamsAccessAuthMiddleware = async ({request, data, env, next}) =&gt; {
    try {
        const userEmail = request.headers.get("cf-access-authenticated-user-email")

        if (!userEmail) {
            throw new Error("User not found, make sure application is behind Cloudflare Access")
        }
  
        // Pass user info to next handlers
        data.user = {
            email: userEmail
        }
  
        return next()
    } catch (e) {
        return new Response(e.toString(), {status: 401})
    }
}

export const onRequest = [
    cfTeamsAccessAuthMiddleware
]</code></pre>
            
    <div>
      <h3>Putting it all together with Pages</h3>
      <a href="#putting-it-all-together-with-pages">
        
      </a>
    </div>
    <p>We have Cloudflare Access controlling our log-in flow. We use the Stream APIs to manage uploading, displaying, and watching videos. We use <a href="https://workers.cloudflare.com/">Workers</a> for managing fetch requests and handling API calls. Now it’s time to tie it all together using <a href="https://pages.cloudflare.com/">Cloudflare Pages</a>!</p><p>Pages provides an easy way to <a href="https://www.cloudflare.com/developer-platform/solutions/hosting/">deploy and host static websites</a>. But now, Pages seamlessly integrates with the Workers platform (link to announcement post). With this new integration, we can deploy this entire application with a single, readable repository.</p>
    <div>
      <h3>Controlling access</h3>
      <a href="#controlling-access">
        
      </a>
    </div>
    <p>Some applications are better public; others contain sensitive data and should be restricted to specific users. The main page is public for this application, and we've used <a href="https://www.cloudflare.com/teams/access/">Cloudflare Access</a> to limit the admin page to employees. You could just as easily use Access to protect the entire application if you’re building an internal learning service or even if you want to beta launch a new site!</p><p>When a user clicks the admin link on our <a href="https://cf-pages-stream.pages.dev/">demo site</a>, they will be prompted for an email address. If they enter a valid Cloudflare email, the application will send them an access code. Otherwise, they won't be able to access that page.</p><p>Check out the <a href="https://github.com/cloudflare/pages-stream-demo">source code</a> and get started building your own video application today!</p> ]]></content:encoded>
            <category><![CDATA[Full Stack Week]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Cloudflare Pages]]></category>
            <category><![CDATA[Cloudflare Access]]></category>
            <category><![CDATA[Cloudflare Stream]]></category>
            <category><![CDATA[Video]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <guid isPermaLink="false">6crZId6sbMVFWUz7qedA31</guid>
            <dc:creator>Jonathan Kuperman</dc:creator>
            <dc:creator>Adam Janiš</dc:creator>
        </item>
    </channel>
</rss>