Infrastructure

The edge service is deployed across two platforms: Cloudflare Workers (compute and routing) and Auth0 (identity provider actions and tenant policy). Cloudflare Workers deployments and route bindings are managed via wrangler.toml and wrangler publish. Supporting infrastructure — DNS records, Auth0 actions, and the GrowthBook proxy — is managed by Terraform from the deploy/ directory. Worker binaries are built with webpack and published via wrangler.

The Terraform state backend is S3 at key terraform/edge/terraform.tfstate.

Deployment

The deploy workflow (.github/workflows/deploy.yaml) authenticates to AWS with GitHub OIDC; there are no static credentials for Terraform. Pull requests assume the read-only site-github-services-plan role and run terraform plan; pushes to main (test) and published releases (prod) assume site-github-services-deploy and run terraform apply. Both roles are owned by the infrastructure repo. AWS is used only for the state backend; the Cloudflare and Auth0 providers use their own API tokens. The FLOATME_AWS_* secrets uploaded to the Workers are the Workers' runtime identity, not the deploy credential, and are unaffected.

Provider versions in use:

  • cloudflare/cloudflare 4.52.5

  • auth0/auth0 1.33.0

  • hashicorp/aws 6.19.0


Environments

Edge supports three named environments controlled by the FLOATME_ENVIRONMENT variable (default: test): test deploys from main, staging deploys from main into the staging AWS account and Auth0 tenants, and prod deploys on release. Every resource is namespaced by environment.

Worker environment split

Each wrangler.toml defines [env.prod], [env.test] and [env.staging] stanzas. The top-level name/route keys are local-dev defaults only. Wrangler is always invoked with --env $FLOATME_ENVIRONMENT so the correct name and hostname are selected at publish time.

Prod and test use routes on hostnames whose DNS records were made by hand; staging uses wrangler custom domains, so Cloudflare creates the record and certificate with the worker.

Worker prod route test route staging custom domain

auth

auth.floatme.io/*

auth.test.floatme.io/*

auth.staging.floatme.io

backoffice-auth

auth.backoffice.floatme.io/*

auth.backoffice.test.floatme.io/*

auth.backoffice.staging.floatme.io

sms

sms.floatme.io/*

sms.test.floatme.io/*

sms.staging.floatme.io

links

links.floatme.io/*

links.test.floatme.io/*

links.staging.floatme.io

Auth0 tenant split

The auth and backoffice-auth workers point to different Auth0 tenants per environment. This is a hard split — each environment uses entirely separate tenants with separate client IDs, management credentials, and MFA audiences.

Worker prod tenant test tenant staging tenant

auth

floatme.auth0.com

floatme-test.us.auth0.com

floatme-staging.us.auth0.com

backoffice-auth

floatme-backoffice.us.auth0.com

floatme-backoffice-test.us.auth0.com

floatme-backoffice-staging.us.auth0.com

Castle blocking (ENABLE_CASTLE_BLOCKING) is set to "true" in prod and "false" elsewhere, so fraud signals are enforced only in production. Code that bypasses checks for QA integration users keys on isTestLike(env) (cloudflare/helpers/environment.ts), which is true for test and staging.

Environment → Worker Deployed Name → Cloudflare hostname
────────────────────────────────────────────────────────────────
prod     auth              prod-auth              auth.floatme.io/*
prod     backoffice-auth   prod-backoffice-auth   auth.backoffice.floatme.io/*
prod     sms               prod-sms               sms.floatme.io/*
prod     links             prod-links             links.floatme.io/*

test     auth              test-auth              auth.test.floatme.io/*
test     backoffice-auth   test-backoffice-auth   auth.backoffice.test.floatme.io/*
test     sms               test-sms               sms.test.floatme.io/*
test     links             test-links             links.test.floatme.io/*

staging  auth              staging-auth           auth.staging.floatme.io
staging  backoffice-auth   staging-backoffice-auth auth.backoffice.staging.floatme.io
staging  sms               staging-sms            sms.staging.floatme.io
staging  links             staging-links          links.staging.floatme.io

Cloudflare Workers

Workers are TypeScript source files. Two workers (auth, sms) are compiled by webpack into ES module bundles under dist/cloudflare/workers/<name>/index.mjs. The backoffice-auth and links workers are published directly from source via wrangler without a webpack build step. Each worker has its own wrangler.toml in cloudflare/workers/<name>/.

Worker Source Wrangler config Purpose

auth

cloudflare/workers/auth/index.ts

cloudflare/workers/auth/wrangler.toml

Auth0 proxy for the FloatMe mobile app. Handles login, signup, MFA, social auth, password reset, session tokens, and Castle fraud checks.

backoffice-auth

cloudflare/workers/backoffice-auth/index.ts

cloudflare/workers/backoffice-auth/wrangler.toml

Auth0 proxy for the internal backoffice admin console. Uses a dedicated Auth0 tenant.

sms

cloudflare/workers/sms/index.ts

cloudflare/workers/sms/wrangler.toml

"Text me the app" (TMTA) worker — sends an SMS with the app download link via Twilio.

links

cloudflare/workers/links/index.ts

cloudflare/workers/links/wrangler.toml

Deeplink rewriter (links.floatme.io → floatme://) and .well-known file host (AASA, assetlinks.json).

Worker environment variables

Non-secret configuration is declared inline in wrangler.toml under [env.<name>.vars]. Secrets are uploaded separately via wrangler secret put (see Secrets Management).

Table 1. auth worker vars
Variable prod value test value

WORKER_ENV

prod

test

AUTH0_BASE_URL

https://floatme.auth0.com

https://floatme-test.us.auth0.com

AUTH0_CLIENT_ID

5fDBKS8dtHEcG5yKGf7y2wgej3ioJ09i

z9L42UYyo4B9uKAoR0yqroenFio819Hx

AUTH0_MANAGEMENT_BASE_URL

https://floatme.auth0.com/api/v2/

https://floatme-test.us.auth0.com/api/v2/

FLOATME_USER_SERVICE_URL

https://j72mm4jbii.execute-api.us-east-2.amazonaws.com

https://7oweo0qx8a.execute-api.us-west-2.amazonaws.com

FLOATME_USER_SERVICE_REGION

us-east-2

us-west-2

CASTLE_DOMAIN

https://api.castle.io

https://api.castle.io/v1 ⚠️ bug — the Castle client appends /v1/…​ paths to this value, so test resolves to /v1/v1/…​. Castle blocking is disabled in test (ENABLE_CASTLE_BLOCKING=false) so it has no runtime impact, but this should be corrected to https://api.castle.io.

ENABLE_CASTLE_BLOCKING

true

false

Staging additionally sets AUTH_BASE_URL/LINKS_BASE_URL (the worker’s own hostnames, replacing hard-coded test values), FLOATME_QA_SERVICE_REGION, and the AWS identity pair FLOATME_AWS_ROLE_ARN and AUTH0_AWS_AUDIENCE. When FLOATME_AWS_ROLE_ARN is set, cloudflare/helpers/aws-credentials.ts signs with temporary credentials from STS (an Auth0 client-credentials token for that audience, exchanged with AssumeRoleWithWebIdentity, cached for the hour-long session) instead of the static FLOATME_AWS_* key. See AWS Identity.

Table 2. backoffice-auth worker vars
Variable prod value test value

AUTH0_BASE_URL

https://floatme-backoffice.us.auth0.com

https://floatme-backoffice-test.us.auth0.com

AUTH0_CLIENT_ID

FmqEiC43Lh4CTmn6Yt7nh0zE7xL4f5EM

XRLwrQ738NdKUIV2YeKmSQhgOAb1UeTF

AUTH0_AUTHENTICATION_AUDIENCE

https://api.backoffice.floatme.io

https://api.backoffice.test.floatme.io

AUTH0_CONNECTION

Username-Password-Authentication

Username-Password-Authentication

Table 3. sms worker vars (same in both environments — single Twilio account)
Variable Value

SMS_TMTA_FROM_NUMBER

+12108641371

SMS_TMTA_TWILIO_MESSAGING_SID

MG6ce087e41e8ac916eda01cacba2090f6

TWILIO_ACCOUNT_SID

ACfb6e3e47c8b47f0c9dc0a8a305705af2

The links worker has no non-secret vars; it uses only WORKER_ENV.


DNS

DNS records are managed in Terraform, all within the floatme.io Cloudflare zone (zone ID supplied via var.cloudflare_zone_id). All records are proxied through Cloudflare.

Resource (Terraform) Type Name (prod) Name (test) Purpose

cloudflare_record.backoffice_auth

AAAA

auth.backoffice.floatme.io

auth.backoffice.test.floatme.io

Discard record (100::, RFC 6666 discard prefix — not IPv6 loopback ::1) — Cloudflare routes the request to the backoffice-auth worker before it ever reaches an origin server. Defined in backoffice_auth_dns.tf.

cloudflare_record.growthbook

CNAME

growthbook.floatme.io

test-growthbook.floatme.io

CNAME to cdn.growthbook.io — routes GrowthBook SDK traffic through Cloudflare’s proxy. Defined in growthbook_proxy.tf.

The auth, sms and links hostnames in test and prod are hand-made records in the Cloudflare dashboard, with the worker routes in each wrangler.toml registered on top when wrangler publishes. Staging uses wrangler custom domains instead, so Cloudflare creates and removes those records with the worker.


GrowthBook Proxy

deploy/growthbook_proxy.tf provisions a Cloudflare DNS CNAME record that points growthbook.floatme.io (prod) or test-growthbook.floatme.io (test) at cdn.growthbook.io. The record is proxied, meaning all GrowthBook SDK requests from FloatMe clients flow through Cloudflare rather than directly to GrowthBook’s CDN.

This provides two benefits:

  1. Cloudflare caches the GrowthBook feature flag payload at the edge, reducing latency for SDK initialization.

  2. FloatMe controls the DNS name, so the GrowthBook CDN URL can be changed without updating client builds.


Auth0 Actions

Auth0 Actions are deployed via Terraform (deploy/auth0_actions.tf). All actions run Node.js 18. They are automatically deployed (deploy = true) when Terraform applies.

Terraform resource Action name (env-prefixed) Trigger Source file

auth0_action.ip_metadata_action

{env}-ip-metadata

post-login v3

auth0/actions/add_ip_to_metadata.js

auth0_action.mfa_action

{env}-mfa-custom-claim

post-login v3

auth0/actions/mfa_action.js

auth0_action.set_mfa_action

{env}-set-mfa

pre-user-registration v2

auth0/actions/enable_mfa.js

The post-login trigger pipeline (auth0_trigger_actions.login_flow) runs ip-metadata first, then mfa-custom-claim, on every post-login event.

The auth0/rules/ directory contains legacy Auth0 rules that have been superseded by Actions and are not deployed or documented.

Secrets Management

Secrets are never stored in wrangler.toml. They are uploaded directly to the Cloudflare Workers secret store using wrangler secret put --env $FLOATME_ENVIRONMENT. Each worker has a secrets.sh script in its source directory.

Run all secrets in one pass:

FLOATME_ENVIRONMENT=prod make secrets.host

Or per-worker:

FLOATME_ENVIRONMENT=prod make secrets.auth.host
FLOATME_ENVIRONMENT=prod make secrets.backoffice-auth.host
FLOATME_ENVIRONMENT=prod make secrets.sms.host
FLOATME_ENVIRONMENT=prod make secrets.links.host
Table 4. Secrets by worker
Worker Secret name (in Cloudflare) Source env var

auth

AUTH0_CLIENT_SECRET

AUTH0_APP_CLIENT_SECRET

auth

AUTH0_CONNECTION

AUTH0_CONNECTION

auth

AUTH0_AUTHENTICATION_AUDIENCE

AUTH0_AUTHENTICATION_AUDIENCE

auth

AUTH0_AUTHENTICATION_ISSUER

AUTH0_AUTHENTICATION_ISSUER

auth

AUTH0_MFA_AUDIENCE

AUTH0_MFA_AUDIENCE

auth

CASTLE_CLIENT_SECRET

CASTLE_CLIENT_SECRET

auth

AUTH0_AWS_CLIENT_ID, AUTH0_AWS_CLIENT_SECRET

AUTH0_AWS_CLIENT_ID, AUTH0_AWS_CLIENT_SECRET (environments on the STS role only)

auth

FLOATME_AWS_ACCESS_KEY_ID

FLOATME_AWS_ACCESS_KEY_ID

auth

FLOATME_AWS_SECRET_ACCESS_KEY

FLOATME_AWS_SECRET_ACCESS_KEY

auth

AUTH0_MANAGEMENT_CLIENT_ID

AUTH0_MANAGEMENT_CLIENT_ID

auth

AUTH0_MANAGEMENT_CLIENT_SECRET

AUTH0_MANAGEMENT_CLIENT_SECRET

auth

AUTH0_MANAGEMENT_AUDIENCE

AUTH0_MANAGEMENT_AUDIENCE

auth

DATADOG_API_KEY

DATADOG_API_KEY

backoffice-auth

AUTH0_CLIENT_SECRET

BACKOFFICE_AUTH0_CLIENT_SECRET

backoffice-auth

DATADOG_API_KEY

DATADOG_API_KEY

sms

TWILIO_AUTH_TOKEN

SMS_TMTA_TWILIO_AUTH_TOKEN

sms

DATADOG_API_KEY

DATADOG_API_KEY

links

DATADOG_API_KEY

DATADOG_API_KEY

The AWS credentials uploaded to the auth worker (FLOATME_AWS_ACCESS_KEY_ID / FLOATME_AWS_SECRET_ACCESS_KEY) are scoped to the IAM user named by var.cloudflare_workers_iam_user_name and are used to sign SigV4 requests to the user-service API Gateway.


Build and Publish Flow

Building workers

make build        # runs webpack inside the Docker devkit container
make build.host   # runs webpack directly on the host (requires Node + webpack)

Both targets invoke webpack and output compiled bundles to dist/cloudflare/workers/<name>/index.js. A final edge.tar.gz is created in dist/ containing all built artifacts.

The webpack config compiles all four workers in a single pass. The build is a prerequisite of the tarball target.

Publishing workers

After building, publish each worker with wrangler:

FLOATME_ENVIRONMENT=prod \
  wrangler publish dist/cloudflare/workers/auth/index.js \
  --env prod \
  -c cloudflare/workers/auth/wrangler.toml

Repeat for each worker. The --env flag selects which wrangler.toml stanza ([env.prod] or [env.test]) to use for the worker name and route pattern.

For local development, wrangler dev can be started per-worker without building:

make auth.local            # wrangler dev --env test (auth worker)
make backoffice-auth.local # wrangler dev --env test (backoffice-auth worker)
make sms.local             # wrangler dev (no env flag — uses top-level defaults)
make links.local           # wrangler dev (no env flag — uses top-level defaults)

Terraform apply flow

All Terraform is in deploy/. The deploy/Makefile exposes three targets driven by the FLOATME_ENVIRONMENT variable:

cd deploy
FLOATME_ENVIRONMENT=prod make init    # terraform init
FLOATME_ENVIRONMENT=prod make plan    # terraform plan
FLOATME_ENVIRONMENT=prod make apply   # terraform apply

The root takes two inputs. FLOATME_ENVIRONMENT is mapped to TF_VAR_environment and CLOUDFLARE_ZONE_ID to TF_VAR_cloudflare_zone_id by the Makefile. Everything else the providers need arrives as their own environment variables: CLOUDFLARE_API_TOKEN, the AUTH0_DOMAIN/AUTH0_CLIENT_ID/AUTH0_CLIENT_SECRET trio for the Auth0 provider, and AWS credentials for the state backend.

The root Makefile also exposes make tf WHAT=<subcommand> which delegates to deploy/Makefile.


Terraform Structure

File Contents

deploy/main.tf

Provider configuration for Cloudflare, Auth0, and AWS. S3 backend definition.

deploy/variables.tf

The two inputs: environment name and the Cloudflare zone ID.

deploy/auth0_actions.tf

Three auth0_action resources (ip-metadata, mfa-custom-claim, set-mfa) and the auth0_trigger_actions.login_flow that orders the post-login pipeline.

deploy/backoffice_auth_dns.tf

cloudflare_record.backoffice_auth — the AAAA discard record that anchors the backoffice-auth worker route. The subdomain is auth.backoffice.floatme.io in prod and auth.backoffice.{env}.floatme.io in all other environments.

deploy/growthbook_proxy.tf

cloudflare_record.growthbook — CNAME to cdn.growthbook.io that routes GrowthBook SDK traffic through Cloudflare.