Skip to main content

Cloudflare Workers Architecture

The platform is built as a multi-worker architecture on Cloudflare's edge network, with each worker handling specific responsibilities.

Key Files

  • crates/bf_resident/src/lib.rs - Main API worker
  • crates/bf_auth/src/lib.rs - Authentication worker
  • crates/bf_notify/src/lib.rs - Notification/webhook worker

Worker Overview

┌─────────────────────────────────────────────────────────────────────────┐
│ Cloudflare Edge Network │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ bf_resident │ │
│ │ (Main API Worker) │ │
│ │ • Property management • Device operations │ │
│ │ • User/tenant handlers • IT setup │ │
│ │ • Portfolio management • Mobile/OTA endpoints │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ bf_auth │ │ bf_notify │ │
│ │ (Authentication) │ │ (Webhooks & Events) │ │
│ │ • OAuth callbacks │ │ • SmartThings webhooks │ │
│ │ • Token management │ │ • Yale webhooks │ │
│ │ • Credential gen │ │ • Upload completion │ │
│ │ • Service tokens │ │ • Queue processing │ │
│ └─────────────────────────┘ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

bf_resident - Main API Worker

The primary entry point for all client requests.

Entry Point

// crates/bf_resident/src/lib.rs

#[event(fetch)]
async fn fetch(
req: HttpRequest,
env: Env,
ctx: Context,
) -> worker::Result<http::Response<axum::body::Body>> {
console_error_panic_hook::set_once();

let mut router = create_role_based_router(env);
let response = router.call(req).await
.map_err(|e| worker::Error::RustError(e.to_string()))?;

Ok(response)
}

Route Organization

Routes are organized by authentication level and role:

fn create_role_based_router(env: Env) -> Router {
Router::new()
// Public routes (no auth)
.nest("/", public_routes())

// Service-to-service routes (SERVICE_AUTH_SECRET)
.nest("/service", service_routes())

// Role-based routes (Clerk JWT + role check)
.nest("/user", user_routes())
.nest("/admin", admin_routes())
.nest("/pm", property_manager_routes())
.nest("/installer", installer_routes())
.nest("/it-admin", it_admin_routes())
.nest("/mobile", mobile_routes())

.with_state(env)
}

Public Routes

No authentication required:

RoutePurpose
/healthHealth check
/.well-known/apple-app-site-associationiOS app linking
/.well-known/assetlinks.jsonAndroid app linking
/invites/acceptInvite acceptance (optional auth)
/api/pending-upload/{upload_id}Upload status tracking

Service Routes

Protected by SERVICE_AUTH_SECRET:

RoutePurpose
/service/lookup/*Property/device ID resolution
/service/sync/*Data synchronization
/service/notify/*Internal notifications

Role-Based Routes

Protected by Clerk JWT + role middleware:

User/Tenant Routes (/user/*):

  • Property access for assigned tenants
  • Guest management
  • Device control
  • Notification preferences

Admin Routes (/admin/*):

  • Full property CRUD
  • User management
  • System configuration

Property Manager Routes (/pm/*):

  • Portfolio-scoped property access
  • Tenant viewing PIN codes
  • Contractor visit management

Installer Routes (/installer/*):

  • Device discovery
  • Commissioning operations
  • Error reporting

IT Admin Routes (/it-admin/*):

  • SmartThings/Yale setup
  • Cloud transfer
  • Credential management

bf_auth - Authentication Worker

Handles OAuth flows, token management, and credential generation.

Entry Point

// crates/bf_auth/src/lib.rs

#[event(fetch)]
async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result<Response> {
let router = create_router(env);
router.run(req, env).await
}

fn create_router(env: Env) -> Router {
Router::new()
// Public routes
.route("/health", get(health_check))
.route("/oauth/yale", get(oauth::handle_oauth_callback))
.route("/webhook/yale", post(webhook::handle_yale_webhook))
.route("/webhook/email", post(handlers::email::handle_email_webhook))
.route("/webhook/sms", post(handlers::sms::handle_sms_webhook))

// Service routes (AUTH_SERVICE_AUTH_SECRET)
.nest("/service", service_routes())

.with_state(env)
}

Service Endpoints

Protected by service authentication:

RoutePurpose
/service/tokenGet OAuth token for property
/service/statusCheck token status
/service/refreshManual token refresh
/service/verification-codeGet TOTP code
/service/credentialsGenerate service credentials
/service/totp/setStore TOTP secret
/service/totp/codeGet current TOTP code
/service/firebase/tokenGet Firebase access token

OAuth Durable Object

Token storage and refresh handled by a Durable Object:

#[durable_object]
pub struct OAuthDurableObject {
state: Arc<State>,
env: Env,
conn: DOConnection,
}

impl DurableObject for OAuthDurableObject {
async fn fetch(&self, req: Request) -> worker::Result<Response> {
// Handle RPC: StoreTokens, GetTokens, RefreshTokens
}

async fn alarm(&self) -> worker::Result<Response> {
// Scheduled token refresh with exponential backoff
}
}

bf_notify - Notification Worker

Processes incoming webhooks and event queues.

Entry Point

// crates/bf_notify/src/lib.rs

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: Context) -> worker::Result<Response> {
Router::new()
.get("/health", |_, _| Response::ok("bf_notify worker healthy"))

// Webhook endpoints
.post_async("/webhooks/smartthings", handle_smartthings_webhook)
.post_async("/webhooks/yale", handle_yale_webhook)
.post_async("/webhooks/bf-smart", handle_bf_smart_webhook)
.post_async("/webhooks/upload-complete", handle_upload_webhook)

// Admin triggers
.post_async("/admin/trigger/energy-aggregation", trigger_energy_aggregation)
.post_async("/admin/trigger/backup", trigger_backup)
.post_async("/admin/trigger/status-check", trigger_status_check)

.run(req, env)
.await
}

#[event(queue)]
pub async fn queue(batch: MessageBatch<serde_json::Value>, env: Env, _ctx: Context) -> Result<()> {
for message in batch.messages()? {
// Process queued events (SmartThings events, R2 uploads)
if let Ok(smart_event) = serde_json::from_value::<QueuedSmartEvent>(body) {
process_queued_smart_event(&smart_event, &env).await?;
}
}
Ok(())
}

Webhook Processing Flow

External Service (SmartThings/Yale)


┌───────────────┐
│ bf_notify │
│ /webhooks/* │
└───────┬───────┘

┌───────▼───────┐
│ Signature │
│ Verification │
└───────┬───────┘

┌───────▼───────┐
│ Queue │
│ (async) │
└───────┬───────┘

┌───────▼───────┐
│ bf_resident │
│ ID lookup │
└───────┬───────┘

┌───────▼───────┐
│ User DO │
│ Side effects │
└───────────────┘

Request Flow Diagram

Complete request lifecycle for a device control operation:

Mobile App

│ POST /user/properties/{id}/devices/{device_id}/control
│ Authorization: Bearer {clerk_jwt}


┌─────────────────────────────────────────────────────────────┐
│ bf_resident │
├─────────────────────────────────────────────────────────────┤
│ 1. ClerkAuthMiddleware │
│ • Extract JWT from cookie or header │
│ • Verify with JWKS (cached) │
│ • Add ClerkAuth to request extensions │
│ │
│ 2. Role Middleware (require_tenant) │
│ • Verify user has tenant role │
│ │
│ 3. Handler │
│ • Extract property_id, device_id from path │
│ • Build RpcRequest::ExecuteSmartCapability │
│ • Call bf_user::proxy() │
└─────────────────────────────────────────────────────────────┘

│ Internal RPC


┌─────────────────────────────────────────────────────────────┐
│ User Durable Object │
├─────────────────────────────────────────────────────────────┤
│ 1. Parse RpcRequest │
│ │
│ 2. Permission check │
│ • Verify user has access to property │
│ • Check property status (must be occupied) │
│ │
│ 3. Execute capability │
│ • Get credentials from storage │
│ • Call bf_smart with capability │
│ │
│ 4. Update device state │
│ │
│ 5. Process side effects │
│ • Create access log │
│ • Send notification │
│ • FCM push │
│ │
│ 6. Return RpcResponse │
└─────────────────────────────────────────────────────────────┘

│ JSON Response


Mobile App

Storage Architecture

D1 Database (Shared State)

Single D1 database shared across all workers:

TablePurpose
propertiesProperty metadata
devicesDevice definitions
roomsRoom assignments
credentialsEncrypted service credentials
credential_setsCredential groupings
it_setupIT commissioning state
portfoliosPortfolio definitions
invitesUser invitations
activity_logsAudit trail
floor_layoutsFloor plan templates
external_device_idsPlatform ID mappings

User Durable Objects (Per-Property)

Each property has isolated DO storage:

TablePurpose
property_metadataCached property info
tenantsCached tenant list
guestsGuest access records
pin_codesYale PIN codes
access_logsLock/unlock history
energy_readingsPower consumption
device_statesCurrent device states
notificationsUser notifications
device_tokensFCM tokens
credentialsCached OAuth tokens

Other Storage

ServicePurpose
R2Document storage (photos, uploads)
KVCache (JWKS, AI analysis, public keys)
QueuesAsync event processing

Worker-to-Worker Communication

Service Authentication

Workers authenticate to each other using service tokens:

// Creating a service token
let authenticator = ServiceAuthenticator::new(env.var("SERVICE_AUTH_SECRET")?);
let token = authenticator.create_token("bf_resident")?;

// Making authenticated request
let response = reqwest::Client::new()
.post(&format!("{}/service/lookup", BF_RESIDENT_URL))
.header("Authorization", format!("Bearer {}", token))
.header("X-Service-Name", "bf_notify")
.json(&request)
.send()
.await?;

Mesh Client Pattern

Type-safe service calls via bf_types::mesh:

// crates/bf_types/src/mesh/client.rs

let mesh = Mesh::new("bf_notify", &env)?;

// Typed client for resident service
let resident = mesh.resident();
let property = resident.lookup_by_smartthings_ids(&request).await?;

// Typed client for smart service
let smart = mesh.smart();
let device = smart.get_device(&device_id).await?;

Environment Bindings

Workers are configured with these bindings in wrangler.toml:

[vars]
# Environment configuration

[[d1_databases]]
binding = "DB"
database_name = "broadford"
database_id = "..."

[[r2_buckets]]
binding = "DOCUMENTS"
bucket_name = "bf-documents"

[[kv_namespaces]]
binding = "CACHE"
id = "..."

[[queues.producers]]
queue = "smart-events"
binding = "SMART_EVENTS_QUEUE"

[[durable_objects.bindings]]
name = "USER_DO"
class_name = "UserDurableObject"

See Also