Inquiries Platform Service
Inquiries Platform Service & Architecture (INQUIRIES_PLATFORM_SERVICE.md)
1. Executive Summary & Platform-Hosted Topology
The Inquiries Platform Service (@siteswarm/service-inquiries) delivers centralized, multi-tenant customer inquiry ingestion, anti-spam bot defense, durable SQLite persistence, and triage workflow tracking across all SiteSwarm client applications (apps/*).
As a canonical platform-hosted capability (CAPABILITY_MANAGEMENT.md, MULTI_WORKER_PLATFORM_SERVICES.md), inquiries infrastructure is centralized in a dedicated Cloudflare Worker isolate backed by Cloudflare D1 persistence (siteswarm-inquiries-db). Client storefronts connect via a dual-ingress architecture supporting both direct browser HTTP submissions and worker-to-worker Cloudflare Service Binding RPC:
flowchart TD subgraph ClientEnvironments["Client Storefronts & Workers (apps/*)"] BrowserDock["Storefront Browser Island\n(mountInquiryForm in @siteswarm/inquiries/client)"] ClientWorker["Client Application Edge Worker\n(Astro SSR / API Route)"] AdminCockpit["Central Command Center & CMS\n(EmDash Reviewer / Operator Cockpit)"] end
subgraph PlatformIsolate["Central Inquiries Worker (services/inquiries)"] direction TB HttpRouter["HTTP Fetch Router\n(POST /api/inquiries, PATCH /api/inquiries/:id/status)\nDynamic CORS Allowlist"] RpcEntry["WorkerEntrypoint RPC (InquiriesRpc)\n(submitInquiry, getInquiry, listInquiries, updateInquiryStatus)"] DomainCore["InquiryDomainService (domain.ts)\n• Honeypot Silent Trap (0-row absorption)\n• Cloudflare Turnstile Server Validation\n• Strict Multi-Tenant Isolation Enforcement"] D1DB[("Cloudflare D1 SQLite\nsiteswarm-inquiries-db\nCompound Index: (tenant_id, created_at)")]
HttpRouter --> DomainCore RpcEntry --> DomainCore DomainCore --> D1DB end
BrowserDock ==>|"Ingress A: Direct Browser HTTP\n(POST https://inquiries.jacobmiller22.com/api/inquiries)"| HttpRouter ClientWorker ==>|"Ingress B: Service Binding Worker RPC\n(env.INQUIRIES_SERVICE.submitInquiry)"| RpcEntry AdminCockpit ==>|"Internal Operations RPC / REST"| RpcEntry2. Core Architectural Principles
- Zero-Drift Ingress Harmonization (RPC vs. HTTP):
- Whether an inquiry originates as a direct browser
fetch()or a server-side Worker RPC call from a client backend, both ingress paths execute through the same underlyingInquiryDomainServiceinstance. - Validation rules, honeypot filters, Turnstile token challenges, and database transformations are shared and 100% deterministic across all ingress paths.
- Whether an inquiry originates as a direct browser
- Invisible Bot Defense & Honeypot Zero-Row Invariant:
- Customer forms do not impose intrusive visual puzzles on authentic users. Invisible Cloudflare Turnstile verification runs unobtrusively in the background.
- Hidden honeypot traps intercept automated spam scrapers without alerting the bot: submissions containing honeypot inputs receive a synthetic
200 OKresponse but trigger zero writes to D1 SQLite.
- Strict Multi-Tenant Partitioning:
- Every inquiry record is scoped to a specific
tenant_idcorresponding to the client application’s canonicalappId. - Cross-tenant querying and mutation are blocked at the domain boundary: a tenant cannot query, view, or modify inquiry records belonging to another tenant.
- Every inquiry record is scoped to a specific
- Durable Immutability with Controlled Triage:
- Original customer input (
payloadand unparsedraw_inputs) is append-only and immutable. - Only the triage lifecycle status (
status) and modification timestamp (updated_at) may transition over time.
- Original customer input (
3. Dual-Ingress Interface Specification
The platform service worker exposes two first-class access interfaces:
3.1 Worker-to-Worker Service Binding RPC (InquiriesRpc)
Client edge workers configure a Cloudflare Service Binding (INQUIRIES_SERVICE) pointing to siteswarm-service-inquiries. The bound InquiriesRpc class extends WorkerEntrypoint<Env>:
export class InquiriesRpc extends WorkerEntrypoint<Env> { /** Submits a validated inquiry record */ async submitInquiry(input: SubmitInquiryInput): Promise<InquiryRecord>;
/** Retrieves a single inquiry by ID, optionally enforcing tenant ownership */ async getInquiry(id: string, tenantId?: string): Promise<InquiryRecord | null>;
/** Lists inquiries partitioned by tenant with pagination support */ async listInquiries( tenantId: string, options?: { limit?: number; offset?: number } ): Promise<InquiryRecord[]>;
/** Transitions an inquiry's triage status with strict tenant validation */ async updateInquiryStatus( id: string, status: InquiryStatus, tenantId: string ): Promise<InquiryRecord>;
// Backward-compatible RPC aliases async submitLead(input: SubmitInquiryInput): Promise<LeadRecord>; async getLead(id: string, tenantId?: string): Promise<LeadRecord | null>; async listLeads(tenantId: string): Promise<LeadRecord[]>;}RPC calls execute entirely within Cloudflare’s edge runtime kernel (workerd) without public DNS resolution, TLS handshake overhead, or manual JSON serialization.
3.2 Public HTTP / REST Ingress (fetch)
Direct browser submissions route to https://inquiries.jacobmiller22.com (or staging equivalent https://inquiries-staging.jacobmiller22.com):
| Method | Path | Ingress Purpose | Auth / Security |
|---|---|---|---|
OPTIONS |
/* |
CORS preflight handler | Dynamic CORS origin check against tenant allowlist |
POST |
/api/inquiries |
Direct customer form submission | Turnstile token verification + honeypot silent trap |
GET |
/api/inquiries/:id |
Fetch specific inquiry by ID | Tenant header / admin API key validation |
GET |
/api/inquiries |
Query inquiries list for tenant | Tenant header (X-SiteSwarm-Tenant-Id) |
PATCH |
/api/inquiries/:id/status |
Mutate triage status (new $\to$ contacted $\to$ archived) |
Strict tenant_id verification against record owner |
GET |
/health |
Isolate liveness and health probe | Public unauthenticated probe (200 OK) |
Backward-Compatible REST Aliases
To guarantee uninterrupted operation for legacy forms, /api/leads and /api/leads/:id/status route seamlessly to the canonical inquiries handler.
4. D1 SQLite Schema & Indexing Architecture
Inquiry persistence is managed by Cloudflare D1 SQLite (services/inquiries/migrations/0001_initial.sql):
CREATE TABLE IF NOT EXISTS inquiries ( id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL, phone TEXT, message TEXT, payload TEXT NOT NULL DEFAULT '{}', raw_inputs TEXT NOT NULL DEFAULT '{}', turnstile_verified INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'new', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);
CREATE INDEX IF NOT EXISTS idx_inquiries_tenant_created_at ON inquiries(tenant_id, created_at);CREATE INDEX IF NOT EXISTS idx_inquiries_status ON inquiries(status);
-- Backward-compatibility view for legacy queriesCREATE VIEW IF NOT EXISTS leads AS SELECT * FROM inquiries;4.1 Compound Indexing Rationale: idx_inquiries_tenant_created_at
In a multi-tenant shared table, standard single-column indexes on tenant_id force the database engine to perform an in-memory sort or temporary B-tree construction when executing chronological tenant queries (WHERE tenant_id = ? ORDER BY created_at DESC).
The compound index (tenant_id, created_at) provides:
- $O(\log N)$ B-Tree Range Scans: Lookups jump directly to the target tenant’s slice and read pre-sorted entries in reverse chronological order.
- Zero In-Memory Sort Overhead: Eliminates SQLite file-sort operations, guaranteeing steady, sub-millisecond query performance as the global table grows to tens of thousands of rows across clients.
- Partition Scanning Efficiency: Queries with
LIMITandOFFSETclauses terminate immediately after scanning the requested page without evaluating rows from other tenants.
4.2 Status Index: idx_inquiries_status
The single-column index on status supports platform-wide operations:
- Instant retrieval of unread (
status = 'new') inquiries for tenant inbox badges. - Cross-tenant operational telemetry and triage workload metrics without table scans.
5. Bot Defense & Anti-Abuse Engineering
The platform implements two complementary defensive mechanisms to protect clients from automated form spam while maintaining zero friction for authentic users:
flowchart TD RawPost["Form Submission POST"] --> HoneypotCheck{"Honeypot Input Populated?\n(company_fax_number, honeypot, _gotcha)"}
HoneypotCheck -- "Yes (Bot Detected)" --> SilentAbsorb["Silent Trap Absorbed\n• Return Synthetic 200 OK\n• status: 'bot_absorbed'\n• 0 Rows Inserted in D1"]
HoneypotCheck -- "No (Legitimate User)" --> SchemaValidate["Zod Schema Validation\n(BaseInquiryInputSchema)"]
SchemaValidate -- "Failed" --> Err400["Return 400 Bad Request\n(validation error details)"] SchemaValidate -- "Passed" --> TurnstileCheck{"Turnstile Verification\n(Cloudflare siteverify)"}
TurnstileCheck -- "Failed / Expired" --> ErrTurnstile["Return 400 Bad Request\n(INVALID_TURNSTILE_TOKEN)"] TurnstileCheck -- "Passed" --> D1Insert["D1 SQLite Persistence\nINSERT INTO inquiries\nstatus: 'new'"]5.1 Invisible Cloudflare Turnstile Verification
- Customer-facing forms embed Cloudflare Turnstile (managed or non-interactive mode).
- On submission, the client provides
cf-turnstile-responseorturnstileToken. - The domain service verifies the token against Cloudflare’s authoritative siteverify API (
https://challenges.cloudflare.com/turnstile/v0/siteverify). - Test Secrets Invariant: For local development and hermetic CI environments, the service recognizes Cloudflare’s deterministic test keys (
1x0000000000000000000000000000000AA), executing the verification step without external network latency.
5.2 Honeypot Silent Trap (Honeypot Zero-Row Invariant)
Automated spam scrapers parse form DOM trees and populate all visible and hidden input fields indiscriminately.
- Trap Deployment: Form templates include a hidden text input (
company_fax_number) styled withdisplay: none;and markedaria-hidden="true",tabindex="-1", andautocomplete="off". Human users never see or interact with this field. - Absorption Logic: If any honeypot field contains a non-empty string,
InquiryDomainServiceintercepts the request:- Returns a synthetic HTTP
200 OKresponse with a generated UUID and status"bot_absorbed". - Zero D1 Persistence: Completely skips D1
INSERTexecution.
- Returns a synthetic HTTP
- Strategic Benefit:
- Preserves Cloudflare D1 write limits and prevents database bloat.
- Denies bot operators any signal that their submission was caught, preventing them from modifying their scraper heuristics.
6. Multi-Tenant Isolation & Zero-Drift Harmonization
6.1 Tenant Context Resolution
Tenant identity is resolved through an explicit hierarchy:
X-SiteSwarm-Tenant-IdorX-Tenant-IdHTTP headers.- URL query parameters (
?tenant=...or?tenantId=...). - Payload body properties (
tenantId,clientApp,tenant_id).
If no valid tenant identifier is resolved, the request fails immediately with HTTP 400 Bad Request (MISSING_TENANT).
6.2 Status Mutation Isolation Check
When modifying an inquiry’s triage status (updateInquiryStatus), the domain service enforces tenant ownership:
// 1. Fetch existing inquiryconst existing = await this.db .prepare("SELECT * FROM inquiries WHERE id = ?") .bind(id) .first<any>();
if (!existing) { throw new InquiryValidationError(`Inquiry with ID "${id}" not found`, { status: 404, code: "INQUIRY_NOT_FOUND", });}
// 2. Enforce strict tenant ownershipif (existing.tenant_id !== trimmedTenantId) { throw new InquiryValidationError( `Multi-tenant isolation violation: tenant "${trimmedTenantId}" is not authorized to mutate inquiry belonging to "${existing.tenant_id}"`, { status: 403, code: "TENANT_MISMATCH", } );}A client or compromised worker cannot view, mutate, or archive inquiry entries belonging to another business tenant.
7. Inquiry Triage Lifecycle & State Machine
Inquiries advance through a 3-state triage workflow:
stateDiagram-v2 [*] --> new: Customer Submits Form new --> contacted: Operator / Business Owner Responds contacted --> archived: Interaction Completed / Closed archived --> contacted: Reopened (Customer Follows Up) contacted --> new: Re-queued (Needs Attention) new --> archived: Marked Irrelevant / Spam| State | Lifecycle Stage | System & Dashboard Behavior |
|---|---|---|
new |
Fresh Intake | Unread entry. Triggers notifications and badges in business owner cockpits. |
contacted |
In Progress | The business owner has initiated phone, SMS, or email contact with the customer. |
archived |
Closed / Completed | Inquiry resolved, booked, or closed. Kept for historical reporting and audit trails. |
8. Data Retention & Portability Invariants
- Payload & Raw Input Preservation:
payload: Parsed JSON containing validated form values (name,email,phone,message, client-specific custom fields).raw_inputs: Complete raw JSON payload as submitted, ensuring that any ad-hoc form fields added during rapid prototype iterations are preserved without data loss.
- Zero-Lockout Client Portability:
- Per
CLIENT_OFFBOARDING.md, any business departing SiteSwarm can export 100% of their historical inquiries in standard SQLite format viaSELECT * FROM inquiries WHERE tenant_id = ?. - Data is never held hostage, encrypted with proprietary client keys, or tied to proprietary SaaS schemas.
- Per
Decisions Intentionally Deferred
- Direct third-party CRM sync (HubSpot, Salesforce, Zoho) is intentionally deferred; outbound webhooks provide the extension seam for custom client CRM forwarding.
- Real-time customer chat or bi-directional threaded messaging is deferred; the inquiries capability focuses exclusively on fast, highly reliable intake and triage.