logicspike/docs

Forms

Forms Service — Permissions

Last Updated: 2026-06-28 Status: Active Companion docs: architecture.md · api-spec.md


1. Permission Strings

Permission Grants access to
forms:read List forms, get a form, per-form analytics, tenant overview, list submissions (all statuses), get a submission, list webhook deliveries
forms:write Create / update / delete / duplicate forms; update submission status + note; delete submissions; reply to a submission; retry webhook delivery
forms:submissions.export GET /admin/forms/:id/submissions/export (CSV / JSON download)
system:owner Bypasses all permission checks; also bypasses credit gates (unlimited submissions)

2. Permission Matrix

Operation forms:read forms:write forms:submissions.export system:owner
GET /admin/forms ✅ ✅ — ✅
GET /admin/forms/overview ✅ ✅ — ✅
GET /admin/forms/:id ✅ ✅ — ✅
GET /admin/forms/:id/analytics ✅ ✅ — ✅
POST /admin/forms — ✅ — ✅
PUT /admin/forms/:id — ✅ — ✅
DELETE /admin/forms/:id — ✅ — ✅
POST /admin/forms/:id/duplicate — ✅ — ✅
GET /admin/forms/:id/submissions ✅ ✅ — ✅
GET /admin/submissions (inbox) ✅ ✅ — ✅
GET /admin/submissions/:id ✅ ✅ — ✅
PATCH /admin/submissions/:id — ✅ — ✅
DELETE /admin/submissions/:id — ✅ — ✅
POST /admin/submissions/:id/reply — ✅ — ✅
GET /admin/forms/:id/webhook-deliveries ✅ ✅ — ✅
GET /admin/forms/:id/submissions/export — — ✅ ✅
MCP tools (read) ✅ ✅ — ✅
MCP tools (write) — ✅ — ✅
MCP export-submissions — — ✅ ✅
Public POST /f/:form_id no auth required no auth required no auth required —

3. Gateway Headers

The gateway injects these headers for all admin and MCP requests:

Header Set by Contains
x-gateway-key Gateway Shared secret (validated against GATEWAY_SECRET). Present on all requests including public.
x-tenant-id Gateway Owning tenant's ID (admin/MCP only)
x-user-id Gateway Authenticated user's ID (admin/MCP only)
x-user-role Gateway User role string (admin/MCP only)
x-user-permissions Gateway Comma-separated permission strings (admin/MCP only)
x-request-id Gateway Unique request ID for tracing

The forms-service reads x-user-permissions and checks for the required permission strings via requirePermission().


4. Public Submit Path

POST /f/:form_id, GET /f/:form_id/schema, GET /f/:form_id/page, and GET /f/embed.js require no auth header. The form_id in the URL is the only credential.

The gateway still injects x-gateway-key on the public path (required by the worker's gateway guard), but does not inject tenant headers. The forms-service derives the tenant from the form row itself.

Abuse prevention on the public path:

  • allowed_origins CORS check (per-form)
  • Honeypot field (always active, default field name botcheck)
  • Cloudflare Turnstile (per-form opt-in, captchaRequired=true)
  • Native rate limiter: 20 requests per 60 seconds per (form_id, IP)

5. Role Bundles

forms:read and forms:write are bundled into the tenant owner, admin, and editor roles. forms:submissions.export is bundled into owner and admin. system:owner bypasses all checks including credit gates.

Permission strings are defined in @repo/core-types and managed in the manager service's entitlement catalog.

Forms