# Services

> Actors, OAuth, asset serving and validation.

<div class="ce-block-head">
<p class="ce-block-lead">Build stateful services, require an OAuth 2.1 token for an API, serve assets, and validate incoming JSON. Container execution has its own dedicated page.</p>
<ul class="ce-facts">
<li><span>Libraries</span> <code>Runtime.Actors</code> <code>Runtime.DynamicWorkflows</code> <code>Runtime.WorkersOauthProvider</code> <code>Runtime.KvAssetHandler</code> <code>Runtime.Cabidela</code> <code>Runtime.Chanfana</code></li>
<li><span>npm</span> See the library table below</li>
<li><span>Free plan</span> 100,000 Worker requests a day</li>
</ul>
</div>

## Image Resizer

The container-backed image resizer now has its own [Containers page](containers.md#image-resizer), including the current F# subclassing limitation.

## Event RSVP

Each invitation is a Durable Object that stores RSVPs in SQLite and responds with the headcount, plus-ones included. The Actors `Storage` helper saves the number of the last migration it applied, so the second migration adds `plus_ones` once to every invitation, old or new. `Invitation` inherits the Workers `DurableObject` class because the current bindings do not support subclassing `Actor` directly from F#.

```fsharp
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Runtime = FSharp.CloudEdge.Runtime.Workers.Cloudflare.Workers
module Actors = FSharp.CloudEdge.Runtime.Actors
module ActorStorage = FSharp.CloudEdge.Runtime.Actors.Storage

type Rsvp = {| email: string; plusOnes: float |}
type Headcount = {| attending: float |}

type Invitation(ctx: Workers.DurableObjectState<obj>, env: obj) =
    inherit Runtime.DurableObject<obj, obj>(ctx, env)

    let schema = ActorStorage.Exports.Storage ctx.storage

    do schema.migrations <- [|
        Actors.SQLSchemaMigration.Create(1., "RSVPs", "CREATE TABLE IF NOT EXISTS rsvps (email TEXT PRIMARY KEY)")
        Actors.SQLSchemaMigration.Create(2., "Plus-ones", "ALTER TABLE rsvps ADD COLUMN plus_ones INTEGER DEFAULT 0")
    |]

    interface Runtime.DurableObject.IFetchHandler with
        member _.fetch request =
            async {
                let! _ = schema.runMigrations() |> Async.AwaitPromise
                if request.``method`` = "POST" then
                    let! rsvp = request.json<Rsvp>() |> Async.AwaitPromise
                    ctx.storage.sql.exec("INSERT OR REPLACE INTO rsvps VALUES (?, ?)", rsvp.email, rsvp.plusOnes) |> ignore
                let headcount = ctx.storage.sql.exec<Headcount>("SELECT COUNT(*) + TOTAL(plus_ones) AS attending FROM rsvps").one()
                return Workers.Exports.Response.json headcount
            }
            |> Async.StartAsPromise
            |> U2.Case1
```

<div class="ce-needs"><p><strong>Needs</strong> a Durable Object binding for <code>Invitation</code> with the <a href="https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/">SQLite storage backend</a>, which <code>ctx.storage.sql</code> requires.</p></div>

:::info
`@cloudflare/actors` 0.0.1-beta.6 is a beta release, and its README states that the project is in active development.
:::

## Account API

`OAuthProvider` publishes its OAuth metadata and implements the token and client registration endpoints. It checks the bearer token on every request under `/api/` before it calls `account`, and `home` answers all other paths. `ctx.props` holds the `props` passed to `completeAuthorization` at sign-in.

```fsharp
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module OAuth = FSharp.CloudEdge.Runtime.WorkersOauthProvider

type User = {| userId: string |}

let account: OAuth.OAuthProviderOptions.ApiHandler<obj> =
    OAuth.OAuthProviderOptions.ApiHandler.Create(
        fetch = fun _ _ (ctx: Workers.ExecutionContext<User>) ->
            Workers.Exports.Response.json {| userId = ctx.props.userId |})

let home: Workers.ExportedHandler<obj, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun _ _ _ -> U2.Case2(Workers.Exports.Response.Create("Sign in to continue")))

[<ExportDefault>]
let provider =
    OAuth.Exports.OAuthProvider(
        OAuth.OAuthProviderOptions.Create(
            defaultHandler = home,
            authorizeEndpoint = "/authorize",
            tokenEndpoint = "/oauth/token",
            clientRegistrationEndpoint = "/oauth/register",
            apiRoute = U2.Case1 "/api/",
            apiHandler = U2.Case2 account))
```

<div class="ce-needs"><p><strong>Needs</strong> a KV namespace bound as <code>OAUTH_KV</code>, where the provider stores grants and tokens.</p></div>

<details class="ce-js"><summary>Emitted JavaScript</summary>

```javascript
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";

export const account = {
    fetch: (_arg, _arg_1, ctx) => globalThis.Response.json({
        userId: ctx.props.userId,
    }),
};

export const home = {
    fetch: (_arg, _arg_1, _arg_2) => (new globalThis.Response("Sign in to continue")),
};

export const provider = new OAuthProvider({
    defaultHandler: home,
    authorizeEndpoint: "/authorize",
    tokenEndpoint: "/oauth/token",
    apiRoute: "/api/",
    apiHandler: account,
    clientRegistrationEndpoint: "/oauth/register",
});

export default provider;
```

</details>

## Consent Page

Set `consent` as the provider's `defaultHandler` in place of `home`. The provider adds `OAUTH_PROVIDER` to `env`. `parseAuthRequest` validates the client and its redirect URI. After your own sign-in and consent steps, `completeAuthorization` stores the grant and returns the `redirectTo` URL.

```fsharp
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module OAuth = FSharp.CloudEdge.Runtime.WorkersOauthProvider

type Env =
    abstract OAUTH_PROVIDER: OAuth.OAuthHelpers

let consent: Workers.ExportedHandler<Env, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request env _ ->
            async {
                let url = Workers.Exports.URL(U2.Case1 request.url)
                if url.pathname <> "/authorize" then
                    return Workers.Exports.Response.Create("Not found", Workers.ResponseInit.Create(status = 404.))
                else
                    let! authRequest = env.OAUTH_PROVIDER.parseAuthRequest request |> Async.AwaitPromise
                    let! client = env.OAUTH_PROVIDER.lookupClient authRequest.clientId |> Async.AwaitPromise
                    let name = client |> Option.bind (fun c -> c.clientName) |> Option.defaultValue authRequest.clientId
                    let scopes = String.concat ", " authRequest.scope
                    return Workers.Exports.Response.Create($"{name} asks for: {scopes}")
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )
```

## Single-Page App

The Worker serves a built front end from Workers KV. `serveSinglePageApp` maps page routes such as `/settings` to `index.html`, so the front end's own router handles them. Because the result of `getAssetFromKV` is typed `obj` in 0.1.0, this Worker is a plain record whose `fetch` returns that result unchanged.

```fsharp
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Assets = FSharp.CloudEdge.Runtime.KvAssetHandler

type Env =
    abstract SITE: Workers.KVNamespace<string>

let options (env: Env) =
    Assets.Options.MapRequestToAsset.Options.Create(
        ASSET_NAMESPACE = env.SITE,
        mapRequestToAsset = Assets.Options.MapRequestToAsset(fun request _ -> Assets.Exports.serveSinglePageApp request))

[<ExportDefault>]
let worker =
    {| fetch = fun (request: obj) (env: Env) (ctx: Workers.ExecutionContext<obj>) ->
        let lookup = Assets.GetAssetFromKV.Event.Create(request, fun work -> ctx.waitUntil work)
        Assets.Exports.getAssetFromKV(lookup, options env) |}
```

<div class="ce-needs"><p><strong>Needs</strong> a KV namespace bound as <code>SITE</code> that holds each file under its path without the leading slash, such as <code>index.html</code> or <code>assets/app.js</code>.</p></div>

<details class="ce-js"><summary>Emitted JavaScript</summary>

```javascript
import { getAssetFromKV, serveSinglePageApp } from "@cloudflare/kv-asset-handler";

export function options(env) {
    return {
        ASSET_NAMESPACE: env.SITE,
        mapRequestToAsset: (request, _arg) => serveSinglePageApp(request),
    };
}

export const worker = {
    fetch: (request, env, ctx) => getAssetFromKV({
        request: request,
        waitUntil: (work) => {
            ctx.waitUntil(work);
        },
    }, options(env)),
};

export default worker;
```

</details>

## Signup Check

The schema is an anonymous record, which Fable emits as a plain JSON Schema object. With `applyDefaults`, Cabidela sets `plan` to `free` if a submission omits it. When `request.json` or `validate` throws, the Worker responds with status 400 and the error message.

```fsharp
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Cabidela = FSharp.CloudEdge.Runtime.Cabidela

let schema =
    {| ``type`` = "object"
       required = [| "email" |]
       properties =
        {| email = {| ``type`` = "string"; minLength = 3 |}
           plan = {| ``type`` = "string"; enum = [| "free"; "team" |]; ``default`` = "free" |} |} |}

let signup = Cabidela.Exports.Cabidela(schema, Cabidela.CabidelaOptions.Create(applyDefaults = true))

[<ExportDefault>]
let worker: Workers.ExportedHandler<obj, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request _ _ ->
            async {
                try
                    let! form = request.json<obj>() |> Async.AwaitPromise
                    signup.validate form |> ignore
                    return Workers.Exports.Response.json form
                with error ->
                    let status = Workers.ResponseInit.Create(status = 400.)
                    return Workers.Exports.Response.json({| error = error.Message |}, U2.Case2 status)
            }
            |> Async.StartAsPromise
            |> U2.Case1
    )
```

<details class="ce-js"><summary>Emitted JavaScript</summary>

```javascript
export const schema = {
    properties: {
        email: {
            minLength: 3,
            type: "string",
        },
        plan: {
            default: "free",
            enum: ["free", "team"],
            type: "string",
        },
    },
    required: ["email"],
    type: "object",
};

export const signup = new Cabidela(schema, ({
    applyDefaults: true,
}));
```

</details>

## API Reference

`getSwaggerUI` and `getReDocUI` each build a standalone HTML page. That HTML loads its viewer from jsDelivr and fetches the OpenAPI document at the URL you pass. This Worker serves the document itself at `/openapi.json` and ReDoc at `/redoc`. Swagger UI is the response for every other path.

Chanfana's routes are `OpenAPIRoute` subclasses registered on a Hono or itty-router app. In 0.1.0 `OpenAPIRoute` is an interface that F# code cannot subclass, and neither router has an F# binding. This Worker calls the two documentation functions directly, since each takes only a URL.

```fsharp
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Chanfana = FSharp.CloudEdge.Runtime.Chanfana

let listTasks = {| summary = "List tasks"; responses = {| ``200`` = {| description = "The task list" |} |} |}

let spec =
    {| openapi = "3.1.0"
       info = {| title = "Tasks API"; version = "1.0.0" |}
       paths = {| ``/tasks`` = {| get = listTasks |} |} |}

let html = Workers.ResponseInit.Create(headers = {| ``content-type`` = "text/html; charset=utf-8" |})

[<ExportDefault>]
let worker: Workers.ExportedHandler<obj, obj, obj, obj> =
    Workers.ExportedHandler.Create(
        fetch = fun request _ _ ->
            match Workers.Exports.URL(U2.Case1 request.url).pathname with
            | "/openapi.json" -> U2.Case2(Workers.Exports.Response.json spec)
            | "/redoc" -> U2.Case2(Workers.Exports.Response.Create(Chanfana.Exports.getReDocUI "/openapi.json", html))
            | _ -> U2.Case2(Workers.Exports.Response.Create(Chanfana.Exports.getSwaggerUI "/openapi.json", html))
    )
```

<details class="ce-js"><summary>Emitted JavaScript</summary>

```javascript
import { getSwaggerUI, getReDocUI } from "chanfana";

export const listTasks = {
    responses: {
        "200": {
            description: "The task list",
        },
    },
    summary: "List tasks",
};

export const spec = {
    info: {
        title: "Tasks API",
        version: "1.0.0",
    },
    openapi: "3.1.0",
    paths: {
        "/tasks": {
            get: listTasks,
        },
    },
};

export const html = {
    headers: {
        "content-type": "text/html; charset=utf-8",
    },
};

export const worker = {
    fetch: (request, _arg, _arg_1) => {
        const matchValue = (new URL(request.url)).pathname;
        return (matchValue === "/openapi.json") ? globalThis.Response.json(spec) : ((matchValue === "/redoc") ? (new globalThis.Response(getReDocUI("/openapi.json"), html)) : (new globalThis.Response(getSwaggerUI("/openapi.json"), html)));
    },
};

export default worker;
```

</details>

## Library Table

| Library | npm package and version | What it covers |
| --- | --- | --- |
| `Runtime.Actors` | `@cloudflare/actors` 0.0.1-beta.6 | Durable Object helpers |
| `Runtime.DynamicWorkflows` | `@cloudflare/dynamic-workflows` 0.1.1 | Workflows per tenant |
| `Runtime.WorkersOauthProvider` | `@cloudflare/workers-oauth-provider` 0.10.3 | OAuth 2.1 provider |
| `Runtime.KvAssetHandler` | `@cloudflare/kv-asset-handler` 0.5.0 | Static files from KV |
| `Runtime.Cabidela` | `@cloudflare/cabidela` 0.2.4 | JSON Schema validation |
| `Runtime.Chanfana` | `chanfana` 3.4.0 | OpenAPI schemas and docs |

Using Dynamic Workflows from F# requires a cast in 0.1.0, so the library has no example here. A Worker Loader stub's `getEntrypoint` is typed `obj`, and `createDynamicWorkflowEntrypoint` takes a loader that must produce a typed `WorkflowRunner`.

## Related Pages

<div class="ce-next">
<a class="ce-next__card" href="/FSharp.CloudEdge/libraries/platform/durable-objects/"><strong>Durable Objects</strong><span>Rooms with storage and alarms</span></a>
<a class="ce-next__card" href="/FSharp.CloudEdge/libraries/platform/storage/"><strong>Storage</strong><span>KV, R2 and D1</span></a>
<a class="ce-next__card" href="/FSharp.CloudEdge/libraries/support/"><strong>Support Libraries</strong><span>Workers helpers</span></a>
<a class="ce-next__card" href="/FSharp.CloudEdge/libraries/compute/"><strong>Compute</strong><span>Sandboxes for running code</span></a>
<a class="ce-next__card" href="/FSharp.CloudEdge/libraries/control-plane/worker-upload/"><strong>Worker Upload</strong><span>A Worker and its bindings</span></a>
</div>

## NuGet packages

[Runtime.Actors 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.Actors/0.1.0), [Runtime.Cabidela 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.Cabidela/0.1.0), [Runtime.Chanfana 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.Chanfana/0.1.0), [Runtime.DynamicWorkflows 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.DynamicWorkflows/0.1.0), [Runtime.KvAssetHandler 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.KvAssetHandler/0.1.0), [Runtime.Workers 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.Workers/0.1.0), [Runtime.WorkersOauthProvider 0.1.0](https://www.nuget.org/packages/FSharp.CloudEdge.Runtime.WorkersOauthProvider/0.1.0).

See [installation and release availability](../guide/packages.md).
