Last Updated: 2026-06-28 Status: Active Service:
apps/forms-serviceCompanion 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 handlerCaching
| 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}, storedstatus=spam - Captcha fails →
400 {success:false, message:"Captcha verification failed"}, storedstatus=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_idis 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 honorsredirect. - 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.