Skip to content

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:

  1. Connects to the platform inquiries capability with verified multi-tenant isolation.
  2. Implements invisible anti-bot defenses (Cloudflare Turnstile and silent honeypot traps).
  3. Configures Worker Service Bindings (INQUIRIES_SERVICE) for zero-latency server-side RPC or direct browser HTTP.
  4. Adheres to platform architectural standards codified in CAPABILITY_MANAGEMENT.md and INQUIRIES_PLATFORM_SERVICE.md.

2. Pre-Flight Checklist

Before beginning onboarding, verify that:

  • The client workspace exists at apps/<client>/ (scaffolded greenfield or promoted per CLIENT_INTAKE_AND_ONBOARDING.md).
  • A unique appId (e.g. 'bakery', 'auto-repair', 'clk-construction') has been assigned.
  • The monorepo capability package @siteswarm/inquiries is registered as a dependency in apps/<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 targets exist 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 mountInquiryForm with endpoint: "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

  1. 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"}'
  2. Assert HTTP 200 OK is returned.
  3. Assert that 0 rows were created in D1 SQLite (status: "bot_absorbed").

7.2 Turnstile Verification Test

  1. 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"}'
  2. Assert HTTP 400 Bad Request with error code MISSING_TURNSTILE_TOKEN (or simulated acceptance when running in dev unbonded mode).

7.3 Multi-Tenant Isolation Audit

  1. 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"}'
  2. Assert HTTP 403 Forbidden (TENANT_MISMATCH).

7.4 Monorepo Governance & Linters

Run the authoritative machine gates:

Terminal window
# Verify TypeScript compilation and governance contracts
pnpm run check
# Verify architectural integrity and phantom service bindings
pnpm run lint:arch

Both commands must exit with 0 errors.