Client Inquiry Intake Onboarding
Client Inquiry Intake Onboarding Runbook (CLIENT_INQUIRY_INTAKE_ONBOARDING.md)
1. Purpose & Lifecycle Overview
This runbook guides engineers and automated agents through enrolling a client application (apps/<client>) into the centralized Inquiries Platform Service (@siteswarm/service-inquiries).
Following this runbook ensures that the client storefront:
- Connects to the platform inquiries capability with verified multi-tenant isolation.
- Implements invisible anti-bot defenses (Cloudflare Turnstile and silent honeypot traps).
- Configures Worker Service Bindings (
INQUIRIES_SERVICE) for zero-latency server-side RPC or direct browser HTTP. - Adheres to platform architectural standards codified in
CAPABILITY_MANAGEMENT.mdandINQUIRIES_PLATFORM_SERVICE.md.
2. Pre-Flight Checklist
Before beginning onboarding, verify that:
- The client workspace exists at
apps/<client>/(scaffolded greenfield or promoted perCLIENT_INTAKE_AND_ONBOARDING.md). - A unique
appId(e.g.'bakery','auto-repair','clk-construction') has been assigned. - The monorepo capability package
@siteswarm/inquiriesis registered as a dependency inapps/<client>/package.json:"dependencies": {"@siteswarm/inquiries": "workspace:*"}
3. Step 1: Client Manifest Configuration (swarm.config.ts)
Declare the inquiries capability in the client application’s authoritative manifest file (apps/<client>/swarm.config.ts):
import { defineAppConfig } from "@siteswarm/governance";
export default defineAppConfig({ appId: "example-client", name: "Example Client",
capabilities: { // Platform-Hosted Inquiries Service Integration inquiries: { type: "platform-hosted", version: "0.1.0", targets: [ "src/components/ContactForm.astro", "src/pages/api/submit-inquiry.ts", ], options: { provider: "turnstile", requireTurnstile: true, notifyChannel: "sms", storeSubmissions: true, honeypotFields: ["company_fax_number", "honeypot", "_gotcha"], successMessage: "Thank you! Your message has been safely received.", defaultStatus: "new", }, storage: { mode: "platform-shared", serviceBindingName: "INQUIRIES_SERVICE", }, }, },});[!NOTE] Ensure all files specified in
targetsexist on disk. Governance audits (pnpm run lint:arch) continuously verify that every declared target path is valid.
4. Step 2: Cloudflare Worker Service Binding (wrangler.jsonc)
To enable server-side Worker RPC communication, declare the INQUIRIES_SERVICE service binding in apps/<client>/wrangler.jsonc:
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "siteswarm-app-example-client", "compatibility_date": "2026-09-29", "compatibility_flags": ["nodejs_compat"],
"services": [ { "binding": "INQUIRIES_SERVICE", "service": "siteswarm-service-inquiries" } ],
"env": { "staging": { "services": [ { "binding": "INQUIRIES_SERVICE", "service": "siteswarm-service-inquiries-staging" } ] }, "production": { "services": [ { "binding": "INQUIRIES_SERVICE", "service": "siteswarm-service-inquiries" } ] } }}Typed Environment Declaration (src/env.d.ts)
Expose the binding type to TypeScript in apps/<client>/src/env.d.ts:
/// <reference path="../.astro/types.d.ts" />
type InquiriesRpc = import("@siteswarm/service-inquiries").InquiriesRpc;
interface Env { INQUIRIES_SERVICE?: InquiriesRpc;}
declare namespace App { interface Locals { runtime: { env: Env; }; }}5. Step 3: Storefront Inquiry Form Setup
SiteSwarm storefront forms follow the Visual Freedom + Shared Core standard (VISUAL_FREEDOM_AND_SHARED_CORE.md). Markup, styles, and typography are tailored bespoke to the client brand, while submission logic delegates to @siteswarm/inquiries/client.
Create or update apps/<client>/src/components/ContactForm.astro:
---// apps/<client>/src/components/ContactForm.astro---
<div class="contact-card"> <h3>Get in Touch</h3> <p>Leave your details and our team will get back to you promptly.</p>
<form id="inquiry-form" method="POST" action="/api/submit-inquiry"> <!-- Antispam Honeypot Field (Hidden from authentic users) --> <div style="display: none;" aria-hidden="true"> <label for="company_fax_number">Do not fill this field</label> <input type="text" id="company_fax_number" name="company_fax_number" tabindex="-1" autocomplete="off" /> </div>
<div class="form-group"> <label for="name">Your Name <span class="required">*</span></label> <input type="text" id="name" name="name" required placeholder="Jane Doe" autocomplete="name" /> </div>
<div class="form-group"> <label for="email">Email Address <span class="required">*</span></label> <input type="email" id="email" name="email" required placeholder="jane@example.com" autocomplete="email" /> </div>
<div class="form-group"> <label for="phone">Phone Number</label> <input type="tel" id="phone" name="phone" placeholder="(555) 012-3456" autocomplete="tel" /> </div>
<div class="form-group"> <label for="message">Message <span class="required">*</span></label> <textarea id="message" name="message" rows="4" required placeholder="How can we help?"></textarea> </div>
<!-- Invisible Cloudflare Turnstile Challenge --> <div class="cf-turnstile" data-sitekey="0x4AAAAAA..." data-size="flexible" data-theme="light" ></div>
<button type="submit" id="submit-btn" class="submit-btn">Send Inquiry</button> <div id="form-feedback" class="form-feedback" role="status" aria-live="polite"></div> </form></div>
<script> import { mountInquiryForm } from "@siteswarm/inquiries/client";
const form = document.getElementById("inquiry-form") as HTMLFormElement | null; if (form) { mountInquiryForm(form, { endpoint: "/api/submit-inquiry", submitButton: "#submit-btn", submittingText: "Submitting Inquiry...", feedbackElement: "#form-feedback", successClass: "form-feedback success", errorClass: "form-feedback error", defaultSuccessMessage: "Thank you! Your inquiry has been safely received.", defaultErrorMessage: "There was an issue submitting your request. Please call us directly.", }); }</script>[!TIP] Direct Browser HTTP (Zero Worker Ingress Option): For purely static edge storefronts with zero SSR worker requirements, configure
mountInquiryFormwithendpoint: "https://inquiries.jacobmiller22.com/api/inquiries". The form will submit directly to the platform service, bypassing the client worker entirely.
6. Step 4: Edge Route Handler Setup (/api/submit-inquiry.ts)
When using SSR or Worker RPC ingress, create an edge route handler at apps/<client>/src/pages/api/submit-inquiry.ts:
import type { APIRoute } from "astro";import { BaseInquiryInputSchema } from "@siteswarm/inquiries/schemas";
export const prerender = false;
export const POST: APIRoute = async (context) => { const cfEnv = (context.locals as any)?.runtime?.env; const inquiriesService = cfEnv?.INQUIRIES_SERVICE;
try { const body = await context.request.json(); const validated = BaseInquiryInputSchema.parse(body);
// 1. Worker RPC Path (Primary) if (inquiriesService && typeof inquiriesService.submitInquiry === "function") { const inquiry = await inquiriesService.submitInquiry({ tenantId: "example-client", ...validated, turnstileToken: body["cf-turnstile-response"] || body.turnstileToken, });
return new Response( JSON.stringify({ success: true, message: "Thank you! Your inquiry has been safely received.", inquiryId: inquiry.id, }), { status: 200, headers: { "Content-Type": "application/json" } } ); }
// 2. Local Fallback (Unbonded local development) return new Response( JSON.stringify({ success: true, message: "[Dev Simulation] Inquiry captured successfully.", inquiryId: "dev-simulated-" + Date.now(), }), { status: 200, headers: { "Content-Type": "application/json" } } ); } catch (err: any) { return new Response( JSON.stringify({ success: false, error: err.message || "Failed to process inquiry submission", issues: err.issues, }), { status: 400, headers: { "Content-Type": "application/json" } } ); }};7. Step 5: Verification & End-to-End Testing
Execute the following verification suite to confirm that onboarding is complete and compliant:
7.1 Honeypot Silent Trap Test
- Send a POST request with the honeypot field populated:
Terminal window curl -X POST http://localhost:<port>/api/submit-inquiry \-H "Content-Type: application/json" \-d '{"name":"Bot Spammer","email":"bot@spam.com","company_fax_number":"555-1234"}' - Assert HTTP
200 OKis returned. - Assert that 0 rows were created in D1 SQLite (
status: "bot_absorbed").
7.2 Turnstile Verification Test
- Submit a request without a Turnstile token:
Terminal window curl -X POST http://localhost:<port>/api/submit-inquiry \-H "Content-Type: application/json" \-d '{"name":"Authentic Customer","email":"customer@example.com","message":"Hello"}' - Assert HTTP
400 Bad Requestwith error codeMISSING_TURNSTILE_TOKEN(or simulated acceptance when running in dev unbonded mode).
7.3 Multi-Tenant Isolation Audit
- Query inquiries with an unauthorized tenant header:
Terminal window curl -X PATCH http://localhost:<port>/api/inquiries/<id>/status \-H "X-SiteSwarm-Tenant-Id: malicious-tenant" \-d '{"status":"archived"}' - Assert HTTP
403 Forbidden(TENANT_MISMATCH).
7.4 Monorepo Governance & Linters
Run the authoritative machine gates:
# Verify TypeScript compilation and governance contractspnpm run check
# Verify architectural integrity and phantom service bindingspnpm run lint:archBoth commands must exit with 0 errors.