Skip to content
IaC Templates

Edge functions

Template · Updated Jul 2026
Validated Jul 2026

Edge functions

This pattern composes Compute, Network, and Block Storage into a regional function gateway on infrastructure you control.

What this template does#

Provisions a single instance running OpenFaaS faasd, a single-binary FaaS gateway that uses containerd and systemd (no Kubernetes), on a floating IP:

  • Compute instance sized for a modest function workload (2 vCPU and 2 GiB RAM by default)
  • Private network, subnet, router, port, and security group; a floating IP on the gateway only
  • A block volume mounted at /data, so function images, faasd state, and TLS certificates live on a volume you can grow rather than on the boot disk
  • cloud-init installs faasd, generates a gateway password on first boot, deploys the nodeinfo starter function, and starts Caddy as the public HTTP/HTTPS front door in front of faasd on 127.0.0.1:8080

No credential ships with this template. faasd generates the gateway basic-auth password on first boot and writes it to /root/faasd-gateway-credentials (readable only by root).

Honest scope#

This gateway runs in one region on a VM you operate. It is a regional function runtime, not a global edge network: Quake AI has no anycast, no global PoPs, and no first-party serverless edge. Functions execute in the region where you deploy the instance. For geographic distribution, front the origin with a third-party CDN.

Alternate engines#

This template leads with OpenFaaS faasd (OpenFaaS project, single VM, systemd + containerd, the "functions on one box" appliance). Spin / SpinKube (CNCF, WebAssembly) fits when you want Wasm functions on Kubernetes; see the Kubernetes cluster template for the cluster path instead of a single VM. Supabase Edge Runtime (Deno-based, MIT) fits when you already run Supabase self-host and want Deno edge functions beside the BaaS stack.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist)No default
flavor_nameInstance size (faasd + containerd in 2 GiB)s1a.small
image_nameOperating system imageUbuntu-24.04
app_nameDisplay name prefix for resourcesedge-functions
caddy_versionCaddy image tag for the public HTTP/HTTPS front door2-alpine
domainPublic domain for automatic HTTPS; empty serves HTTP on the floating IP""
volume_sizeBlock volume size in GiB, mounted at /data20
external_networkExternal network for floating IP allocationPublicStatic
private_cidrCIDR for the private subnet10.42.0.0/24

Ports and access#

PortPurpose
22Host SSH for administration
80HTTP entry through Caddy to faasd
443HTTPS when domain is set and Caddy has obtained a certificate
8080faasd gateway (localhost only; Caddy is the public entry)

Starter function and deploy workflow#

On first boot, cloud-init deploys the nodeinfo function from the OpenFaaS store. Invoke it at http://<floating-ip>/function/nodeinfo (or HTTPS on your domain when domain is set) with basic auth (admin and the password from /root/faasd-gateway-credentials).

Deploy more functions with faas-cli after you SSH in and log in:

bash
faas-cli login -u admin -p YOUR_PASSWORD --gateway http://YOUR_FLOATING_IP
faas-cli store deploy figlet

When to use this pattern#

Run a regional function gateway on a VM you operate so HTTP handlers execute close to your other Quake AI workloads without adopting a managed serverless edge product. Pair it with Edge reverse proxy when you need TLS termination without functions, or with API gateway when you need rate limits and key auth in front of long-running services instead of functions.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

Starting template$14.70/mo

Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.

What each resource is for

Functions

s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50/mo

Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.

Included in baseline

s1a.small

2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50

Compute + RAM rate basis

2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (40 GiB)

40 GiB at $0.08/GiB/mo

$3.20

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

Included at no charge

These line items are zero on Quake AI. Many other providers meter them separately.

Data transfer (inbound and outbound)

Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.

AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.

Learn more
$0.00

Private networking

Private networks, subnets, Neutron routers, and security groups are included with the plan.

VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.

$0.00

Control-plane API requests

OpenStack API calls for provisioning and management are included.

Some managed services on other clouds meter API calls or charge for premium control-plane features.

$0.00

Pricing data last validated: . For current rates, check quake.ai/pricing.

Template source#

This is a validated OpenTofu template.

8 files. Download the zip or expand to copy any file.Download edge-functions.zip
Show source (8 files)
main.tfHCL
locals {
  caddy_site = var.domain != "" ? var.domain : ":80"
}

data "openstack_images_image_v2" "os" {
  name        = var.image_name
  most_recent = true
}

data "openstack_networking_network_v2" "external" {
  name = var.external_network
}

resource "openstack_networking_network_v2" "private" {
  name           = "${var.app_name}-net"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "${var.app_name}-subnet"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}

resource "openstack_networking_router_v2" "main" {
  name                = "${var.app_name}-router"
  external_network_id = data.openstack_networking_network_v2.external.id
}

resource "openstack_networking_router_interface_v2" "private" {
  router_id = openstack_networking_router_v2.main.id
  subnet_id = openstack_networking_subnet_v2.private.id
}

resource "openstack_networking_secgroup_v2" "functions" {
  name        = "${var.app_name}-sg"
  description = "SSH and HTTP/HTTPS for the regional edge functions gateway"
}

resource "openstack_networking_secgroup_rule_v2" "ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.functions.id
}

resource "openstack_networking_secgroup_rule_v2" "http" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 80
  port_range_max    = 80
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.functions.id
}

resource "openstack_networking_secgroup_rule_v2" "https" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 443
  port_range_max    = 443
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.functions.id
}

resource "openstack_networking_port_v2" "functions" {
  name               = "${var.app_name}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.functions.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_blockstorage_volume_v3" "data" {
  name = "${var.app_name}-data"
  size = var.volume_size
}

resource "openstack_networking_floatingip_v2" "functions" {
  pool = var.external_network
}

resource "openstack_compute_instance_v2" "functions" {
  name        = var.app_name
  flavor_name = var.flavor_name
  key_pair    = var.key_name

  user_data = var.domain != "" ? templatefile("${path.module}/cloud-init/functions-https.yaml.tftpl", {
    caddy_site    = local.caddy_site
    caddy_version = var.caddy_version
    floating_ip   = openstack_networking_floatingip_v2.functions.address
    domain        = var.domain
  }) : templatefile("${path.module}/cloud-init/functions-http.yaml.tftpl", {
    caddy_version = var.caddy_version
    floating_ip   = openstack_networking_floatingip_v2.functions.address
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 20
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.functions.id
  }
}

resource "openstack_compute_volume_attach_v2" "data" {
  instance_id = openstack_compute_instance_v2.functions.id
  volume_id   = openstack_blockstorage_volume_v3.data.id
}

resource "openstack_networking_floatingip_associate_v2" "functions" {
  floating_ip = openstack_networking_floatingip_v2.functions.address
  port_id     = openstack_networking_port_v2.functions.id
}
variables.tfHCL
variable "key_name" {
  description = "SSH keypair name (must already exist in your project)"
  type        = string
}

variable "flavor_name" {
  description = "Instance size. faasd and containerd run on 2 vCPU and 2 GiB RAM for a modest function workload. Raise the flavor when you deploy many functions or use larger container images."
  type        = string
  default     = "s1a.small"
}

variable "image_name" {
  description = "Operating system image. cloud-init targets a Debian-family distribution; Ubuntu 24.04 is the recommended base."
  type        = string
  default     = "Ubuntu-24.04"
}

variable "app_name" {
  description = "Display name prefix for compute and network resources"
  type        = string
  default     = "edge-functions"
}

variable "caddy_version" {
  description = "Caddy container image tag used as the public HTTP/HTTPS front door in front of the faasd gateway on localhost:8080."
  type        = string
  default     = "2-alpine"
}

variable "domain" {
  description = "Public domain for the function gateway. When set, Caddy obtains a Let's Encrypt certificate and terminates TLS before faasd. Point the domain's DNS A record at the floating IP before traffic arrives. Leave empty to serve plain HTTP on port 80 at the floating IP."
  type        = string
  default     = ""
}

variable "volume_size" {
  description = "Block volume size in GiB for faasd function state, container images, and TLS certificates. Mounted at /data."
  type        = number
  default     = 20
}

variable "external_network" {
  description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
  type        = string
  default     = "PublicStatic"
}

variable "private_cidr" {
  description = "CIDR for the private tenant network the function gateway lives in"
  type        = string
  default     = "10.42.0.0/24"
}
outputs.tfHCL
output "instance_id" {
  description = "ID of the compute instance running the edge functions gateway"
  value       = openstack_compute_instance_v2.functions.id
}

output "floating_ip" {
  description = "Public floating IP address of the edge functions gateway"
  value       = openstack_networking_floatingip_v2.functions.address
}

output "private_ip" {
  description = "Private IP address of the gateway on the tenant network"
  value       = openstack_compute_instance_v2.functions.access_ip_v4
}

output "gateway_url" {
  description = "URL for the function gateway. HTTPS on the domain when set, otherwise HTTP on the floating IP. Point DNS at the floating IP before relying on automatic TLS."
  value       = var.domain != "" ? "https://${var.domain}" : "http://${openstack_networking_floatingip_v2.functions.address}"
}
versions.tfHCL
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.0"
    }
  }
}

provider "openstack" {}
terraform.tfvars.exampleHCL
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"

# Recommended: set a domain so Caddy obtains a Let's Encrypt certificate in front
# of the faasd gateway. Point its DNS A record at the floating IP from the outputs.
# domain = "functions.example.com"

# caddy_version = "2-alpine"
# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# app_name = "edge-functions"
# volume_size = 20
# external_network = "PublicStatic"
# private_cidr = "10.42.0.0/24"
cloud-init/functions-http.yaml.tftpl
#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
  - jq
  - openssl
write_files:
  - path: /opt/edge-functions/Caddyfile
    permissions: "0644"
    content: |
      :80 {
          reverse_proxy 127.0.0.1:8080
      }
  - path: /opt/edge-functions/docker-compose.yml
    permissions: "0644"
    content: |
      services:
        caddy:
          image: caddy:${caddy_version}
          restart: unless-stopped
          network_mode: host
          volumes:
            - /opt/edge-functions/Caddyfile:/etc/caddy/Caddyfile:ro
            - /data/caddy:/data
            - /data/caddy-config:/config
  - path: /opt/edge-functions/faasd-bootstrap.sh
    permissions: "0755"
    content: |
      #!/usr/bin/env bash
      set -euo pipefail

      CRED_FILE=/root/faasd-gateway-credentials
      GATEWAY_PUBLIC="http://${floating_ip}"
      STACK_DIR=/opt/edge-functions

      if [ -f "$CRED_FILE" ]; then
        systemctl start faasd-provider faasd-gateway 2>/dev/null || true
        cd "$STACK_DIR"
        docker compose up -d
        exit 0
      fi

      mkdir -p /data/faasd /data/caddy /data/caddy-config /var/lib
      if ! mountpoint -q /var/lib/faasd; then
        mount --bind /data/faasd /var/lib/faasd
        grep -q '/data/faasd' /etc/fstab || echo '/data/faasd /var/lib/faasd none bind 0 0' >> /etc/fstab
      fi

      curl -sfL https://raw.githubusercontent.com/openfaas/faasd/master/hack/install.sh | bash

      for i in $(seq 1 90); do
        if curl -sf -o /dev/null http://127.0.0.1:8080/healthz; then
          break
        fi
        sleep 2
      done

      PASSWORD="$(cat /var/lib/faasd/secrets/basic-auth-password)"
      export OPENFAAS_URL=http://127.0.0.1:8080
      export OPENFAAS_PASSWORD="$PASSWORD"

      faas-cli login -u admin -p "$PASSWORD" --gateway "$OPENFAAS_URL"
      faas-cli store deploy nodeinfo --gateway "$OPENFAAS_URL"

      cd "$STACK_DIR"
      docker compose up -d

      for i in $(seq 1 30); do
        if curl -sf -o /dev/null -u "admin:$PASSWORD" "$GATEWAY_PUBLIC/function/nodeinfo"; then
          break
        fi
        sleep 2
      done

      umask 077
      cat > "$CRED_FILE" <<CRED
      OpenFaaS faasd gateway credentials (generated on first boot)
      username: admin
      password: $PASSWORD
      gateway_url: $GATEWAY_PUBLIC
      starter_function: $GATEWAY_PUBLIC/function/nodeinfo

      faas-cli login -u admin -p <password> --gateway $GATEWAY_PUBLIC
      Deploy more functions with faas-cli store deploy or faas-cli deploy.
      CRED
      chmod 600 "$CRED_FILE"
runcmd:
  - |
    set -e
    DEV=/dev/sdb
    for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
    if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L faasddata "$DEV"; fi
    mkdir -p /data
    mount "$DEV" /data
    grep -q "$DEV" /etc/fstab || echo "$DEV /data ext4 defaults,nofail 0 2" >> /etc/fstab
    curl -fsSL https://get.docker.com | sh
    systemctl enable --now docker
    /opt/edge-functions/faasd-bootstrap.sh
cloud-init/functions-https.yaml.tftpl
#cloud-config
package_update: true
packages:
  - ca-certificates
  - curl
  - jq
  - openssl
write_files:
  - path: /opt/edge-functions/Caddyfile
    permissions: "0644"
    content: |
      ${caddy_site} {
          reverse_proxy 127.0.0.1:8080
      }
  - path: /opt/edge-functions/docker-compose.yml
    permissions: "0644"
    content: |
      services:
        caddy:
          image: caddy:${caddy_version}
          restart: unless-stopped
          network_mode: host
          volumes:
            - /opt/edge-functions/Caddyfile:/etc/caddy/Caddyfile:ro
            - /data/caddy:/data
            - /data/caddy-config:/config
  - path: /opt/edge-functions/faasd-bootstrap.sh
    permissions: "0755"
    content: |
      #!/usr/bin/env bash
      set -euo pipefail

      CRED_FILE=/root/faasd-gateway-credentials
      GATEWAY_PUBLIC="https://${domain}"
      STACK_DIR=/opt/edge-functions

      if [ -f "$CRED_FILE" ]; then
        systemctl start faasd-provider faasd-gateway 2>/dev/null || true
        cd "$STACK_DIR"
        docker compose up -d
        exit 0
      fi

      mkdir -p /data/faasd /data/caddy /data/caddy-config /var/lib
      if ! mountpoint -q /var/lib/faasd; then
        mount --bind /data/faasd /var/lib/faasd
        grep -q '/data/faasd' /etc/fstab || echo '/data/faasd /var/lib/faasd none bind 0 0' >> /etc/fstab
      fi

      curl -sfL https://raw.githubusercontent.com/openfaas/faasd/master/hack/install.sh | bash

      for i in $(seq 1 90); do
        if curl -sf -o /dev/null http://127.0.0.1:8080/healthz; then
          break
        fi
        sleep 2
      done

      PASSWORD="$(cat /var/lib/faasd/secrets/basic-auth-password)"
      export OPENFAAS_URL=http://127.0.0.1:8080
      export OPENFAAS_PASSWORD="$PASSWORD"

      faas-cli login -u admin -p "$PASSWORD" --gateway "$OPENFAAS_URL"
      faas-cli store deploy nodeinfo --gateway "$OPENFAAS_URL"

      cd "$STACK_DIR"
      docker compose up -d

      for i in $(seq 1 60); do
        if curl -sfk -o /dev/null -u "admin:$PASSWORD" "$GATEWAY_PUBLIC/function/nodeinfo"; then
          break
        fi
        sleep 3
      done

      umask 077
      cat > "$CRED_FILE" <<CRED
      OpenFaaS faasd gateway credentials (generated on first boot)
      username: admin
      password: $PASSWORD
      gateway_url: $GATEWAY_PUBLIC
      starter_function: $GATEWAY_PUBLIC/function/nodeinfo

      faas-cli login -u admin -p <password> --gateway $GATEWAY_PUBLIC
      Deploy more functions with faas-cli store deploy or faas-cli deploy.
      Point ${domain} DNS at ${floating_ip} before you rely on automatic TLS.
      CRED
      chmod 600 "$CRED_FILE"
runcmd:
  - |
    set -e
    DEV=/dev/sdb
    for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
    if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L faasddata "$DEV"; fi
    mkdir -p /data
    mount "$DEV" /data
    grep -q "$DEV" /etc/fstab || echo "$DEV /data ext4 defaults,nofail 0 2" >> /etc/fstab
    curl -fsSL https://get.docker.com | sh
    systemctl enable --now docker
    /opt/edge-functions/faasd-bootstrap.sh
README.mdMarkdown
# Edge functions

Single compute instance running [OpenFaaS faasd](https://github.com/openfaas/faasd), a lightweight FaaS gateway on infrastructure you control. faasd runs on a floating IP and executes functions in-region on the VM you operate.


**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.

The instance provisions a private network, a floating IP, and a block volume mounted at `/data` so function images, registry state, and TLS certificates live on a resizable volume rather than the boot disk. cloud-init installs containerd and faasd via the upstream install script, generates a gateway password on first boot, deploys the `nodeinfo` starter function from the OpenFaaS store, and starts Caddy as the public HTTP/HTTPS front door in front of the faasd gateway on `127.0.0.1:8080`.

## Where this fits

This is the self-hosted equivalent of Cloudflare Workers or Vercel Edge Functions at the **runtime** layer: you operate the function gateway on a regional VM. It does not replace a global edge network. Functions execute in the region where you deploy the instance. For geographic distribution, [front the origin with a third-party CDN](/docs/network/how-to/front-with-cdn).

## Prerequisites

- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)

## Resource baseline

faasd and containerd are modest on CPU but disk-bound as you add functions. The default `s1a.small` flavor (2 shared vCPU, 2 GiB RAM) handles a starter workload. The boot disk is 20 GiB; function state lives on the separate data volume (`volume_size`, default 20 GiB).

## Usage

1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and set `key_name` (and `domain` if you have one)
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`

cloud-init takes several minutes on first boot to install faasd, deploy the starter function, and start Caddy. The gateway then answers on `gateway_url` from the outputs.

## Gateway credentials and starter function

No credential ships with this template. faasd generates the gateway basic-auth password on first boot and writes it to `/root/faasd-gateway-credentials` (readable only by root). The bootstrap script also deploys the `nodeinfo` function from the OpenFaaS store so `http://<floating-ip>/function/nodeinfo` returns a response after first boot (use HTTPS when `domain` is set).

Deploy additional functions with `faas-cli` after you SSH in and read the credentials file.

## Domain and TLS

With `domain` set and its DNS A record pointed at `floating_ip`, Caddy obtains a Let's Encrypt certificate automatically and proxies HTTPS traffic to faasd on localhost. With `domain` empty, Caddy serves plain HTTP on port 80 at the floating IP.

## Alternate engines

[Spin / SpinKube](https://spinframework.dev) (CNCF, WebAssembly, sub-ms cold starts) fits when you want Wasm functions on Kubernetes; see the [Kubernetes cluster template](/resources/iac-templates/k8s-cluster) for the cluster path. [Supabase Edge Runtime](https://github.com/supabase/edge-runtime) (Deno-based, MIT) fits when you already run [Supabase self-host](/resources/iac-templates/supabase-selfhost) and want its edge functions beside the BaaS stack.

## Outputs

| Output | Description |
| --- | --- |
| `floating_ip` | Public floating IP of the function gateway |
| `private_ip` | Private IP of the gateway on the tenant network |
| `gateway_url` | HTTPS on the domain when set, otherwise HTTP on the floating IP |
| `instance_id` | Compute instance ID |

## Validation

This template passes `tofu validate` in CI. That check confirms the OpenTofu configuration is well-formed against the provider schema; it does not run `tofu apply` against a live account.

Recorded command:

```bash
cd iac/templates/edge-functions && tofu init -backend=false && tofu validate
```
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • key_namerequired
  • flavor_name="s1a.small"
  • image_name="Ubuntu-24.04"
  • app_name="edge-functions"
  • caddy_version="2-alpine"
  • domain=""
  • volume_size=20
  • external_network="PublicStatic"
  • private_cidr="10.42.0.0/24"

Customize this pattern#

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 08.07.2026

Was this page helpful?