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