logicspike/docs

Forms

Forms Service — Architecture

Last Updated: 2026-06-28 Status: Active Service: apps/forms-service Companion docs: forms-vision.md · api-spec.md · database.md · permissions.md · billing.md · embedding.md · webhooks.md


1. What This Service Is

Forms Service is vlozi's multi-tenant form backend — a Web3Forms-grade capture layer described in forms-vision.md. A tenant creates a form, gets a public form_id, and embeds a plain <form> (or AJAX/JSON call, or the SDK) on any website. The service filters spam, stores the submission, bills credits, notifies the owner via email, fires webhooks, and exposes analytics.

It consumes existing platform services: identity (Contact Intelligence), email delivery (Communication), file storage (Media/R2), and auth/rate-limiting (Gateway). Net-new code lives only in apps/forms-service.

NOTE

v1 delivers Web3Forms parity (Pillar 1 — Capture + basic Action). The Intelligence pillar (enrichment, lead scoring, intent/mood) and advanced Action (newsletter opt-in, chatbot handoff, auto-reply) are later phases — see forms-vision.md §9.


2. Where It Sits in the Platform


3. Tech Stack

Layer Technology
Runtime Cloudflare Workers
Framework Hono v4
Database Neon PostgreSQL (serverless)
ORM Drizzle ORM (drizzle-orm/neon-serverless)
Validation Zod + @hono/zod-validator
Migrations drizzle-kit
Spam Honeypot field · Cloudflare Turnstile · native Workers rate-limit binding (20 req/60s per form+IP)
File uploads Media Service /internal/upload (max 5 files, 25 MB each)
Email notify Communication Service /internal/send (Resend) — via service binding
Credit billing Core DB (tenant_credits, credit_transactions) — 0.25 cr per accepted submission
Auth (public) form_id in the URL is the public identifier (safe in client HTML); tenant derived from the form row
Auth (admin/MCP) Gateway-issued x-gateway-key + JWT tenant context headers (x-tenant-id, x-user-id, x-user-permissions)

4. Three Request Surfaces

Surface Caller Auth Routes
Public submit Any website / visitor form_id in URL (public credential) + per-form allowed_origins / honeypot / Turnstile / rate-limit POST /f/:form_id, GET /f/:form_id/schema, GET /f/:form_id/page, GET /f/embed.js
Admin REST Seller dashboard (logged-in tenant) JWT → gateway → x-gateway-key + tenant context /admin/forms/*, /admin/submissions/* CRUD + inbox + analytics
MCP tools AI agents / LLM clients Same JWT + tenant context as admin /mcp/tools/* (15 tools mirroring the REST surface)

All three surfaces reach forms-service only through the gateway — direct worker access is rejected unless the x-gateway-key matches GATEWAY_SECRET.

IMPORTANT

The public submit path does not use the gateway apiKeyMiddleware. The form_id is itself the public credential (like Web3Forms' /submit/{form_id}) — safe to embed in client HTML. Abuse is contained by allowed_origins (CORS), honeypot, Turnstile, and native rate limiting — not by a secret.


5. Middleware Pipeline

Requests pass through the following layers in order (same pattern as apps/blog-service):

1. Logger            → hono/logger
2. DB Middleware     → create/cache Neon connection from FORMS_DATABASE_URL
3. Gateway Guard     → validate x-gateway-key against GATEWAY_SECRET
                       hydrate RequestContext from trusted headers
                       (admin: x-tenant-id, x-user-id, x-user-permissions)
4a. Public submit    → load form by :form_id (30s in-isolate cache)
                       → derive tenant_id from form row
                       → allowed_origins (CORS) check
                       → Spam Guard (honeypot → rate limit → Turnstile if enabled)
                       → credit gate (read tenant_credits; 5min in-isolate cache)
4b. Admin / MCP      → requireTenant → requirePermission("forms:read|write|submissions.export")
5. Route handler

Caching

Cache TTL Key Notes
Form definition (in-isolate Map) 30 s form_id Includes negative lookups (404). Edits propagate within window, not instantly.
Monthly plan limit (in-isolate Map) 5 min tenant_id From core DB plan_service_limits. Used for cap display only; hard cap is credit balance.

6. Submission Flow (Public POST /f/:form_id)

Failure outcomes:

  • Honeypot tripped → silent 200 {success:true}, stored status=spam
  • Captcha fails → 400 {success:false, message:"Captcha verification failed"}, stored status=spam
  • Credit insufficient → 402 Payment Required
  • Validation error → 400 {success:false, message, errors}
  • Rate limit → 429 {success:false, message:"Too many requests"}
  • Origin not allowed → 403 {success:false, message:"Origin not allowed"}

Response shape is Web3Forms-compatible so existing Web3Forms front-ends work unchanged. Full matrix in api-spec.md.


7. Service Boundaries

Concern Owner
Form CRUD, submission ingestion, spam filtering, settings, webhook fan-out, analytics Forms Service (apps/forms-service)
CORS (any-origin for /f/*), gateway-key injection, JWT auth Gateway (apps/gateway)
Form-level abuse control: allowed_origins, honeypot, Turnstile, per-form rate limit Forms Service
Email / Slack notifications, submission reply emails Communication Service (/internal/send, x-internal-key header)
File uploads (images, documents) Media Service (/internal/upload, x-internal-token header)
Credit billing, balance, ledger Core DB (tenant_credits, credit_transactions)
Newsletter opt-in Newsletter Service (planned Phase 4)
Contact merge, lead scoring, intent (Phase 3) Contact Intelligence / AI Brain

NOTE

Two different internal auth headers are in use: Communication Service uses x-internal-key (env INTERNAL_KEY); Media Service uses x-internal-token (env INTERNAL_SERVICE_TOKEN). Both are distinct secrets that must be configured separately.


8. Gateway Integration

api.vlozi.app/forms/f/*     → CORS (any-origin, POST/GET/OPTIONS)
                               → forward (no auth mw) → forms-service /f/*
 
api.vlozi.app/forms/admin/* → authMiddleware (JWT) → buildDownstreamHeaders
                               → forms-service /admin/*
 
api.vlozi.app/forms/mcp/*   → authMiddleware (JWT) → buildDownstreamHeaders
                               → forms-service /mcp/*

Headers forwarded by buildDownstreamHeaders(): x-gateway-key, x-request-id. Admin/MCP paths also inject: x-tenant-id, x-user-id, x-user-permissions. The public path additionally passes through inbound origin, cf-connecting-ip, user-agent, and referer.

Service binding in apps/gateway/wrangler.toml:

[[services]]
binding = "FORMS_SERVICE"
service = "vlozi-forms-service"

9. Environment & Bindings

Variable / Binding Required Purpose
FORMS_DATABASE_URL Neon connection string (forms tables)
GATEWAY_SECRET Validate x-gateway-key from gateway
CORE_DATABASE_URL ⚠️ Neon connection for credit gating/metering. If unset, credit checks are skipped (fail-open — see billing.md).
INTERNAL_KEY ⚠️ x-internal-key shared secret for Communication Service /internal/send calls
INTERNAL_SERVICE_TOKEN ⚠️ x-internal-token shared secret for Media Service /internal/upload calls
TURNSTILE_SECRET_KEY ⚠️ Server-side Turnstile siteverify (required for any form using captchaRequired)
COMMS_SERVICE (binding) Service binding → logicspike-communication
MEDIA_SERVICE (binding) Service binding → logicspike-media
FORM_RATE_LIMITER (binding) Native Workers [[ratelimits]] binding — 20 req/60s per form+IP
DEBUG "true" enables verbose query logging

Dev port: 8796 (wrangler dev).


10. Design Constraints (v1)

  • No new auth primitive — the public form_id is the credential (no secret, no api-key middleware change); the admin path reuses the existing JWT/gateway flow.
  • Web3Forms response compatibility — public submit returns { success, message, data } and honors redirect.
  • Stateless worker — all state in Neon; form definitions cached per-isolate with 30s TTL.
  • Dashboard-first delivery — every submission is stored and visible in the inbox; email is an opt-in per-form setting, never the only record.
  • Spam without friction — honeypot + rate limit are always on; Turnstile is per-form opt-in for high-spam endpoints.
  • Best-effort side effects — notifications, webhook delivery, and credit metering run in waitUntil() and never block the response to the visitor.
Forms