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 workercrates/bf_auth/src/lib.rs- Authentication workercrates/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:
| Route | Purpose |
|---|---|
/health | Health check |
/.well-known/apple-app-site-association | iOS app linking |
/.well-known/assetlinks.json | Android app linking |
/invites/accept | Invite acceptance (optional auth) |
/api/pending-upload/{upload_id} | Upload status tracking |
Service Routes
Protected by SERVICE_AUTH_SECRET:
| Route | Purpose |
|---|---|
/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:
| Route | Purpose |
|---|---|
/service/token | Get OAuth token for property |
/service/status | Check token status |
/service/refresh | Manual token refresh |
/service/verification-code | Get TOTP code |
/service/credentials | Generate service credentials |
/service/totp/set | Store TOTP secret |
/service/totp/code | Get current TOTP code |
/service/firebase/token | Get 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:
| Table | Purpose |
|---|---|
properties | Property metadata |
devices | Device definitions |
rooms | Room assignments |
credentials | Encrypted service credentials |
credential_sets | Credential groupings |
it_setup | IT commissioning state |
portfolios | Portfolio definitions |
invites | User invitations |
activity_logs | Audit trail |
floor_layouts | Floor plan templates |
external_device_ids | Platform ID mappings |
User Durable Objects (Per-Property)
Each property has isolated DO storage:
| Table | Purpose |
|---|---|
property_metadata | Cached property info |
tenants | Cached tenant list |
guests | Guest access records |
pin_codes | Yale PIN codes |
access_logs | Lock/unlock history |
energy_readings | Power consumption |
device_states | Current device states |
notifications | User notifications |
device_tokens | FCM tokens |
credentials | Cached OAuth tokens |
Other Storage
| Service | Purpose |
|---|---|
| R2 | Document storage (photos, uploads) |
| KV | Cache (JWKS, AI analysis, public keys) |
| Queues | Async 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
- sqlx-d1-patterns.md - Database access patterns
- user-durable-object.md - DO architecture details
- service-to-service-auth.md - Service auth details