Every audience that talks to your CRM talks to the same substrate. The staff operator running the platform. The tenant admin curating the customer base. The customer signing in to their own portal. The channel partner reading the deck you shared. The prospect filling in a form on your marketing site. The visitor reading a help article.

Six audiences. One data plane. Four hosts.

The boundary between them lives in the infrastructure — different Dockerfiles, different containers, different Caddy hosts, different origin cookies. A vulnerability discovered in the form renderer physically cannot reach an authenticated admin session because the code does not exist at that origin.

Four hosts, six audiences

console.{base} serves the staff operator console — cross-tenant, session_kind === "staff_platform", MFA-everywhere, sidebar hidden by design. app.{base} serves the tenant admin SPA — the surface where curators of the customer base spend their days. portal.{base} serves three audiences on one host: customers in the Client Portal, channel partners in the Partner Portal, and the general public reading a form, a knowledge-base article, or a gated asset. embed.{base} serves the framework-free embeddable widget — the one your tenant drops on their marketing site.

Each host is its own build artefact. Each carries its own __Host--prefixed HttpOnly cookie whose scope physically does not overlap the others. The admin SPA’s session cookie lives on app.{base} and stays scoped there. The partner cookie lives on portal.{base} and stays scoped there. Browser same-origin policy is the outermost isolation boundary; a server-side session filter scopes every downstream query.

Substrate-level tenant isolation is the industry-standard bar. The origin boundary ships alongside it.

Twelve properties on every public submission

Every form submission on portal.{base} — customer inquiry, KB feedback, partner registration, gated-asset request — passes through twelve enterprise-grade defensive properties before it lands as a row:

  1. Body-size cap.
  2. Idempotency-Key dedupe on a one-hour trailing window.
  3. Silent honeypot.
  4. Accepted-only per-IP rate limit.
  5. hCaptcha verification with encrypted at-rest secret.
  6. Email-domain allow-list.
  7. Origin allow-list.
  8. Field-shape validation.
  9. HMAC-SHA256-signed outbound webhooks.
  10. DNS-TXT vanity-domain ownership challenge.
  11. Tenant isolation at every query.
  12. Forensic capture of any unhandled exception in server_errors.

Bot traffic that fails honeypot, rate-limit, or captcha is never persisted. The inbound_submissions table stays proportional to legitimate traffic. The twelve properties live at the API layer, so any tenant that swaps the portal SPA for their own implementation inherits every one.

The embed widget — ~30 KB, closed shadow DOM

embed.{base} ships a framework-free JavaScript bundle at ~30 KB. It renders into a closed shadow root, so host-page CSS cannot leak in and widget state cannot leak out. Analytics scripts on the host page cannot read the visitor’s typed input. The widget only POSTs to the Actionary API on submit.

Tenants embedding third-party form widgets hit three recurring problems: CSP rule churn, style bleed, and analytics leakage. Shadow-DOM isolation closes all three at once. Drop it on a page; it renders natively and stays quarantined.

Filter chips the agent can author

The SPA’s filter chips are URL-authorable. ?filters[stage]=qualified&filters[owner]=me&filters[created][after]=2026-01-01 deep-links a filtered view of any entity list. Bookmarkable. Shareable. And agent-authorable — when the embedded agent decides the operator’s next task is “open the deals stuck in negotiation for more than 30 days”, it composes the URL and hands it to the operator. Entity-scoped resets, localStorage persistence, and one contract every consumer honours.

Help centre — repo-versioned, palette-searchable

Help articles are Markdown files in the repository. A startup loader parses them into a per-audience corpus (user | admin | both). A three-endpoint HTTP surface feeds the SPA; a Cmd+K palette group surfaces them anywhere in the product. Two MCP tools — search_help_articles and get_help_article — hand the same corpus to the agent, so “how do I set up SSO?” is answered by reading the same article an admin would open. One source of truth across product and agent.

Rich text without a hole in the wall

Any textarea field on any entity — Account.notes, Deal.description, Contact.bio — is one Form Designer click away from being a TipTap rich-text editor. Bold, italic, headings, lists, links.

Every save runs through shared/sanitize.sanitize_user_html — a fourteen-tag allowlist (p, br, strong, em, u, ul, ol, li, code, pre, h2, h3, blockquote, a[href]) with a URL filter that rejects javascript:, data:, vbscript:, protocol-relative, and backslash-escaped variants. DOMPurify runs the same allowlist in the browser with a forced rel="noopener noreferrer" on every anchor. CSP blocks inline scripts as a third backstop. Three independently-sufficient layers of defence. Every surface in the product sanitises user-supplied HTML against a closed allowlist at write time.

Vanity domains and dedicated share codes

A tenant that wants their portal at partners.acme.com points a CNAME. A DNS-TXT ownership challenge verifies the domain; Caddy issues the certificate and starts serving. The admin surface at app.{base} stays routed on its own hostname; the vanity domain reaches only the portal container, because the admin code is not deployed at that origin.

Partner content shares use the same record_access primitive as every other share. A dedicated share code — one row on record_access with grantee_type='partner', one sidecar on record_access_external_recipients — scopes a private download to a single audit-trailed recipient. Same S3 bucket, same presigned-URL machinery, same audit log as the internal share flow. Every share flows through one primitive. Full partner-channel story here.

The takeaway

The substrate does the work. Six audiences read it through four role-appropriate hosts. Every write is idempotent, every session is origin-scoped, every submission passes twelve defensive checks, and every field that accepts HTML runs a closed allowlist end-to-end. The agent has tool parity with the admin SPA across twenty-two public-surface MCP toolslist_inbound_forms, create_inbound_form, list_kb_articles, publish_public_asset, share_asset_with_partners, and the rest. Enterprise buyers ask where the boundary lives; the answer is the infrastructure.