Skip to content

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"| RpcEntry

2. Core Architectural Principles

  1. 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 underlying InquiryDomainService instance.
    • Validation rules, honeypot filters, Turnstile token challenges, and database transformations are shared and 100% deterministic across all ingress paths.
  2. 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 OK response but trigger zero writes to D1 SQLite.
  3. Strict Multi-Tenant Partitioning:
    • Every inquiry record is scoped to a specific tenant_id corresponding to the client application’s canonical appId.
    • Cross-tenant querying and mutation are blocked at the domain boundary: a tenant cannot query, view, or modify inquiry records belonging to another tenant.
  4. Durable Immutability with Controlled Triage:
    • Original customer input (payload and unparsed raw_inputs) is append-only and immutable.
    • Only the triage lifecycle status (status) and modification timestamp (updated_at) may transition over time.

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 queries
CREATE 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:

  1. $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.
  2. 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.
  3. Partition Scanning Efficiency: Queries with LIMIT and OFFSET clauses 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-response or turnstileToken.
  • 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.

  1. Trap Deployment: Form templates include a hidden text input (company_fax_number) styled with display: none; and marked aria-hidden="true", tabindex="-1", and autocomplete="off". Human users never see or interact with this field.
  2. Absorption Logic: If any honeypot field contains a non-empty string, InquiryDomainService intercepts the request:
    • Returns a synthetic HTTP 200 OK response with a generated UUID and status "bot_absorbed".
    • Zero D1 Persistence: Completely skips D1 INSERT execution.
  3. 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:

  1. X-SiteSwarm-Tenant-Id or X-Tenant-Id HTTP headers.
  2. URL query parameters (?tenant=... or ?tenantId=...).
  3. 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 inquiry
const 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 ownership
if (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

  1. 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.
  2. Zero-Lockout Client Portability:
    • Per CLIENT_OFFBOARDING.md, any business departing SiteSwarm can export 100% of their historical inquiries in standard SQLite format via SELECT * FROM inquiries WHERE tenant_id = ?.
    • Data is never held hostage, encrypted with proprietary client keys, or tied to proprietary SaaS schemas.

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.