# Basestack curated Feature Flags and Forms agent handbook Optional bulk context for attachment/offline reading. This is not the entire Basestack documentation corpus. Prefer the index and a focused product skill when URL fetching is available. Links are absolute; all allowlisted skill/reference bodies are included below, so offline reading has no missing companion-file dependency. Last reviewed: 2026-10-16. Source audit: a4c71adee4fed598bb56824156c960615d7c3482. Generated deterministically from the public Markdown allowlist; dates change only on content review. Public context grants no account access or permission for live writes. --- Source: https://docs.basestack.co/skills/basestack-feature-flags/SKILL.md # Basestack Feature Flags Last reviewed: 2026-10-16. Release baseline: flags-js 1.2.3, flags-react 1.2.4, flags-wc 1.0.4, openfeature-provider 1.0.7, vercel-flags-sdk-adapter 1.0.6, flags-cli 1.0.5. Check the consumer's installed versions before applying examples. These are reviewed versions, not instructions to upgrade an existing application automatically. ## When to use Use this guide to add flag evaluation, boolean gating, or payload-driven configuration to an application. Read only the references needed for the task. Public documentation supplies knowledge, not account access or permission to change resources. MCP is not required. ## Required inputs - Framework, runtime, package manager, installed SDK versions, and existing setup. - Real project key, environment key, flag slug, and hosted or self-hosted API URL. - Intended safe disabled/error behavior and the expected payload schema. - Browser versus server evaluation, cache freshness needs, and test environment. If resources are missing, explain how to create the project, environment, and flag in the [dashboard](https://flags.basestack.co/), using the [project guide](https://docs.basestack.co/content/feature-flags/user-guide/projects) and [flag guide](https://docs.basestack.co/content/feature-flags/user-guide/flags). Do not fabricate keys or create/toggle production resources without approval. ## Choose an SDK or integration - Browser, Bun, Node, Vue, or Svelte: `@basestack/flags-js` and [JavaScript](https://docs.basestack.co/skills/basestack-feature-flags/references/javascript.md). No dedicated Basestack Vue/Svelte SDK. - React hooks/components: `@basestack/flags-react/client` and [React](https://docs.basestack.co/skills/basestack-feature-flags/references/react.md). Reuse an existing provider. - Next.js server evaluation or hydration: `@basestack/flags-react/server` and [server frameworks](https://docs.basestack.co/skills/basestack-feature-flags/references/server-frameworks.md); also read [React](https://docs.basestack.co/skills/basestack-feature-flags/references/react.md) for the client boundary. - Existing OpenFeature app: `@basestack/openfeature-provider/server` or `/web` with its corresponding OpenFeature peer SDK; read [OpenFeature](https://docs.basestack.co/skills/basestack-feature-flags/references/openfeature.md). - Existing Vercel Flags SDK app: `@basestack/vercel-flags-sdk-adapter` plus `flags`; read [Vercel Flags SDK](https://docs.basestack.co/skills/basestack-feature-flags/references/vercel-flags-sdk.md). - Preview opt-ins and feedback UI: `@basestack/flags-wc`; read [Web Components](https://docs.basestack.co/skills/basestack-feature-flags/references/web-components.md). Not an evaluation replacement. - Source-reference scanning/upload: `@basestack/flags-cli`; read [CLI](https://docs.basestack.co/skills/basestack-feature-flags/references/cli.md). This is not a flag-management CLI. - Another language or no SDK: read [REST](https://docs.basestack.co/skills/basestack-feature-flags/references/rest-api.md). ## Workflow 1. Inspect the application before choosing a package or adding dependencies. 2. Confirm the project/environment/slug and the fallback with the user. 3. Initialize once at the application boundary, or at a deliberate request boundary on the server. Never share one tenant's configuration across tenants. 4. Keep runtime imports separate. Use `baseURL` for flags-js/React, `apiUrl` for OpenFeature, and `endpoint` for the Vercel adapter. Do not invent shared options. 5. Gate on validated data and use a disabled fallback. JavaScript HTTP failures can return `{ enabled: false, error: true, errorMessage }`; network/JSON failures can throw. Handle both. Validate payloads at runtime, even with TypeScript generics. 6. Explain caching: clearing local SDK cache does not purge API/CDN caches. Never promise instantaneous propagation or streaming updates. 7. Add mocked tests, review browser bundles/serialized props, then verify in the intended environment before rollout. Obtain permission for live requests. ## Verification Test enabled, disabled, missing slug, malformed payload, unsuccessful HTTP, network/parse failures, loading, and stale-cache paths. Verify correct environment selection and no duplicate initialization. A type assertion is not validation. If a reference cannot be fetched, ask for that file or the complete skill directory; do not guess its API. ## Safety constraints - Project/environment evaluation identifiers, project write API tokens, and account-level MCP tokens are not interchangeable. Read evaluation does not require a write token. Keep private API/MCP tokens server-side or in CI secrets. - Browser-consumed flags and payloads are not secret. Do not serialize sensitive payloads, API tokens, or MCP tokens into HTML, hydration scripts, or client props. - Feature flags are not authorization controls. Enforce permissions on the server. Local preview opt-ins can override UI behavior and must never bypass security. - Do not promise targeting, percentage rollouts, experimentation, or streaming unless the specific released product contract has been separately verified. - Source scanning uploads source snippets and repository metadata. Require deliberate opt-in, review the destination and scan paths, and protect tokens. - Do not install packages, mutate accounts, enable production flags, or send feedback merely because this public guide is available. Human setup: [Using these files with an AI assistant](https://docs.basestack.co/content/feature-flags/guides/ai-agents). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/javascript.md # JavaScript, Bun, Node, Vue, and Svelte Last reviewed: 2026-10-16. Applies to `@basestack/flags-js@1.2.3` (ESM; Node >=18.17.0, Bun, or a modern browser with fetch). Use the existing app's package manager; Bun installation is: ```sh bun add @basestack/flags-js@1.2.3 ``` ## Initialize once and fail closed The following module exports a reusable integration. Pass real dashboard keys from the application's configuration; they are not private write tokens. ```ts import { FlagsSDK, type SDKConfig } from "@basestack/flags-js"; export function createCheckoutFlags(config: SDKConfig) { const client = new FlagsSDK({ ...config, baseURL: "https://flags-api.basestack.co/v1", cache: { enabled: true, ttl: 30_000, maxSize: 100 }, preloadFlags: ["new-checkout"], }); async function readCheckout(): Promise { try { const flag = await client.getFlag("new-checkout"); return !flag.error && flag.enabled === true; } catch { return false; } } return { client, readCheckout }; } ``` Override `baseURL` deliberately for a self-hosted deployment. `fetchImpl` accepts a fetch implementation; do not confuse it with the adapters' `fetch` option. `await client.init()` preloads `preloadFlags`, or calls `getAllFlags()` when no slugs are configured. It memoizes its initialization promise (including a rejected one). It is not a polling or retry method. For a single on-demand flag, `getFlag(slug)` alone is sufficient. `getAllFlags()` normally returns `{ flags: Flag[] }`. Check `Array.isArray(response.flags)` before consuming it: an HTTP error can return an error-shaped value despite the declared return type. `init()` resolving is not proof every requested flag was valid. ## Errors and payloads Non-2xx responses with parseable JSON return `{ enabled: false, error: true, errorMessage }`. Network failures and malformed JSON can throw. A missing flag is not equivalent to a successful disabled flag for diagnostics, even when both render the safe UI. Do not log credentials or sensitive response bodies. `payload` is unknown. Validate its shape before applying it: ```ts export function checkoutLabel(payload: unknown): string { if ( typeof payload === "object" && payload !== null && "label" in payload && typeof payload.label === "string" && payload.label.length <= 80 ) { return payload.label; } return "Continue"; } ``` ## Cache and framework boundaries Default in-memory cache: enabled, TTL 300,000 ms, maximum 100 entries. It is per client/process, not shared distributed state. `clearFlagCache(slug)` clears matching single-flag entries; it does not invalidate the all-flags snapshot. Use `clearCache()` when both are relevant, then explicitly refetch. `cache: { enabled: false }` bypasses this cache but not upstream API/CDN caches. Browser Vite apps read public configuration from `import.meta.env.VITE_BASESTACK_PROJECT_KEY` and `import.meta.env.VITE_BASESTACK_ENVIRONMENT_KEY`. Node/Bun servers use validated `process.env.BASESTACK_PROJECT_KEY` and `process.env.BASESTACK_ENVIRONMENT_KEY`. Never put a private API/MCP token in a `VITE_` variable. Vue composables and Svelte stores can wrap this same client. Construct it once, load at the framework's appropriate browser/server boundary, and expose loading, error, and disabled states. No `@basestack/flags-vue` or Svelte package is required. Avoid a process-global client for request-varying tenant keys. Verify mocked enabled/disabled/error-valued responses, rejected fetch, invalid JSON, malformed payloads, and TTL/invalidation. Do not assume timestamps are `Date` instances: JSON transport delivers strings and the SDK does not revive them. Human docs: [JavaScript](https://docs.basestack.co/content/feature-flags/sdks/frontend/javascript), [Node](https://docs.basestack.co/content/feature-flags/sdks/backend/node). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/react.md # React client integration Last reviewed: 2026-10-16. Targets published `@basestack/flags-react@1.2.4`, which depends on flags-js 1.2.3 and flags-wc ^1.0.4. Its published peers are `react >=19.2.7` and `react-dom >=18.2.0`. Do not infer React 18 compatibility from an older README or a workspace manifest sharing this version number. Check the consumer's React/React DOM pairing before installation. ```sh bun add @basestack/flags-react@1.2.4 ``` ## Provider and safe gate Use `/client` for hooks, provider, and components. Reuse an existing provider rather than nesting duplicate clients. This complete client module takes real keys as props so it works with Next.js public configuration or Vite configuration. The stable memoized config prevents a client rebuild on every render. ```tsx "use client"; import { FlagsProvider, useFlag } from "@basestack/flags-react/client"; import { useMemo, type ReactNode } from "react"; export function AppFlags({ projectKey, environmentKey, children, }: { projectKey: string; environmentKey: string; children: ReactNode; }) { const config = useMemo( () => ({ projectKey, environmentKey, baseURL: "https://flags-api.basestack.co/v1", cache: { enabled: false }, }), [projectKey, environmentKey], ); return {children}; } export function CheckoutGate() { const result = useFlag("new-checkout", { defaultEnabled: false }); if (result.isLoading) return

Loading checkout…

; if (result.error || result.flag?.error || !result.enabled) { return

Standard checkout

; } const payload = result.payload; const label = typeof payload === "object" && payload !== null && "label" in payload && typeof payload.label === "string" ? payload.label : "New checkout"; return

{label}

; } ``` Mount `CheckoutGate` inside `AppFlags` at the app's existing root/provider boundary. Disabled and unavailable paths intentionally render the established UI. The example disables SDK cache to make explicit refetches meaningful; it does not bypass upstream caching. Configure a TTL if application needs favor caching. ## Hook and component contracts - `useFlag(slug, { defaultEnabled, defaultPayload, fetch })` returns `flag`, `enabled`, `payload`, `isLoading`, `error`, `refresh`, and `openFeedbackModal`. A generic only asserts a payload type; it does not validate received JSON. - HTTP error values may not populate the hook's `error`; also inspect `flag.error` and keep disabled defaults. Network/parse failures use the error path. - `useFlags()` returns `flags` (array), `flagsBySlug` (record), `isLoading`, `error`, and `refresh`. Do not destructure it as a plain array. - `Feature` accepts `slug`, `fallback`, `loading`, and the hook options. With ordinary children it chooses the enabled/disabled branch. For explicit error checks use a render-function child, which is called before its built-in loading branch, and handle loading inside that function: ```tsx import { Feature } from "@basestack/flags-react/client"; export function CheckoutFeature() { return ( {(result) => result.isLoading ? (

Loading checkout…

) : result.enabled && !result.error && !result.flag?.error ? (

New checkout

) : (

Standard checkout

) }
); } ``` `FlagsProvider` defaults to preloading all flags. `config.preloadFlags` narrows that list; `preload={false}` skips preload. Nonempty `initialFlags` seeds the provider and skips its initial fetch; an empty list is not a completed snapshot. ## Refresh, security, and tests `useFlag().refresh()` refetches through the underlying SDK; it does not clear its cache. `useFlagsClient()` exposes `clearCache()`/`clearFlagCache(slug)` before an explicit refresh. `useFlags().refresh()` can reject, so catch it in UI handlers. No automatic streaming or polling is promised by this provider. Local preview state (`bs-flags-preview-state`) can make `enabled` true. This is user-controlled UI behavior, never authorization. Browser keys and payloads are visible to visitors; private write/API/MCP tokens never belong in provider config. Test loading, enabled, disabled, absent slug, HTTP error value, rejected fetch, malformed payload, refresh, and hydration. See [server frameworks](https://docs.basestack.co/skills/basestack-feature-flags/references/server-frameworks.md) for server imports and initial data. Human docs: [React](https://docs.basestack.co/content/feature-flags/sdks/frontend/react). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/server-frameworks.md # Server frameworks and hydration Last reviewed: 2026-10-16. Basestack contracts target published `@basestack/flags-react@1.2.4` / flags-js 1.2.3. Check the installed Next.js or TanStack Start release before adapting framework boundaries. Read [React](https://docs.basestack.co/skills/basestack-feature-flags/references/react.md) for client provider behavior and peer requirements. ## Exact server exports Import from `@basestack/flags-react/server`: - `createServerFlagsClient(config: SDKConfig): FlagsSDK` - `fetchFlag(slug: string, config: SDKConfig): Promise` - `fetchFlags(config: SDKConfig, slugs?: string[], options?: { fallback?: Flag[]; onError?: (error: unknown) => void }): Promise` - `FlagsHydrationScript` (optional inline serialization, not required below). `fetchFlag` creates a new client per call. It may return an HTTP error value or throw. `fetchFlags` handles thrown failures with a fallback, but fulfilled single-flag error values still need inspection. Neither helper guarantees fresh upstream data. Do not import hooks or browser components in a server-only module. ## Next.js App Router: server evaluation This page is for a server-rendered app, not a static-export-only application. For a static export, server evaluation happens at build time; use browser evaluation or deliberately rebuild when flags change. ```tsx import { fetchFlag } from "@basestack/flags-react/server"; export default async function CheckoutPage() { const projectKey = process.env.BASESTACK_PROJECT_KEY; const environmentKey = process.env.BASESTACK_ENVIRONMENT_KEY; if (!projectKey || !environmentKey) return

Standard checkout

; try { const flag = await fetchFlag("new-checkout", { projectKey, environmentKey, baseURL: "https://flags-api.basestack.co/v1", cache: { enabled: false }, fetchImpl: (input, init) => fetch(input, { ...init, cache: "no-store" }), }); return (

{!flag.error && flag.enabled === true ? "New checkout" : "Standard checkout"}

); } catch { return

Standard checkout

; } } ``` This disables the local SDK and Next fetch caches, not the API/CDN cache. Follow your Next.js version's route rendering policy to avoid accidentally prerendering a flag decision intended to run per request. ## Initial flags across the server/client boundary If client hooks need the same initial decision, obtain a narrow list using `await fetchFlags(config, ["new-checkout"], { fallback: [] })`. Filter out error values and validate `slug`, `enabled`, and payload shape before crossing the boundary. Strip sensitive payloads entirely. For Pages Router JSON props, normalize transport timestamps explicitly rather than assuming SDK `Date` types. ```tsx "use client"; import { FlagsProvider, type Flag } from "@basestack/flags-react/client"; import { useMemo, type ReactNode } from "react"; export function HydratedFlags({ initialFlags, projectKey, environmentKey, children, }: { initialFlags: Flag[]; projectKey: string; environmentKey: string; children: ReactNode; }) { const config = useMemo( () => ({ projectKey, environmentKey }), [projectKey, environmentKey], ); return ( {children} ); } ``` The server passes only reviewed public flags and evaluation identifiers to this component, never the full server config. With `preload={false}`, a missing flag can still be fetched by `useFlag` unless its `fetch` option is false. Keep the same initial values on server and client to avoid hydration mismatches. An inline `FlagsHydrationScript` is another option, but requires the same data review and the application's CSP nonce handling. Never interpolate raw JSON into an ad hoc script. ## Pages Router and TanStack Start In Next.js Pages Router, evaluate in `getServerSideProps` with the same server imports and error checks. Return only a safe boolean or reviewed JSON-compatible snapshot in `props`. `getStaticProps` evaluates at build/revalidation time, not on every request. Use a `/client` provider in the existing `_app` boundary only if hooks are needed. For TanStack Start, use `createServerFn({ method: "GET" }).handler(...)` from `@tanstack/react-start` to wrap a server-only module using these same helpers. Call it from a route loader; return only the validated public decision/snapshot, not credentials. This follows the [official server-function boundary](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions). Do not call an ordinary loader server-only: loaders can run in the browser. Server configuration is server-only `process.env` (or the deployment's server env adapter). Vite client configuration is `import.meta.env.VITE_*`; Next.js browser configuration uses explicitly public `process.env.NEXT_PUBLIC_*`. Never prefix private API/MCP tokens with either public prefix. Verify enabled/disabled/missing/error paths, initial-render consistency, route rendering/cache timing, and absence of tokens or sensitive payloads in page source, serialized props, scripts, and browser bundles. Enforce authorization independently on the server. Human docs: [Next.js](https://docs.basestack.co/content/feature-flags/sdks/backend/nextjs). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/openfeature.md # OpenFeature providers Last reviewed: 2026-10-16. Targets `@basestack/openfeature-provider@1.0.7` (ESM, Node >=18.17.0 or fetch-capable runtime). Published peers: `@openfeature/server-sdk ^1.20.2` and `@openfeature/web-sdk ^1.7.3`; install the peer for the chosen runtime. The root entrypoint exports shared utilities, not a universal provider. ## Server: asynchronous evaluation ```sh bun add @basestack/openfeature-provider@1.0.7 @openfeature/server-sdk@^1.20.2 ``` ```ts import { OpenFeature } from "@openfeature/server-sdk"; import { createBasestackServerProvider } from "@basestack/openfeature-provider/server"; export async function startServerFlags( projectKey: string, environmentKey: string, ) { await OpenFeature.setProviderAndWait( createBasestackServerProvider({ projectKey, environmentKey, apiUrl: "https://flags-api.basestack.co/v1", cacheTtlMs: 30_000, refreshIntervalMs: 0, }), ); const client = OpenFeature.getClient("basestack-server"); return { checkout: await client.getBooleanValue("new-checkout", false), theme: await client.getStringValue("theme", "light"), layout: await client.getObjectValue("layout", { columns: 1 }), }; } ``` Initialize once per application/provider domain, not on every evaluation. Handle startup rejection in application bootstrap and keep the disabled/default experience available. The server provider defaults to no prefetch and a 30 s TTL; evaluations fetch missing/expired flags asynchronously. Set `prefetch: true` if readiness must include loading the snapshot. A refresh interval is opt-in. ## Web: ready snapshot, synchronous evaluation ```sh bun add @basestack/openfeature-provider@1.0.7 @openfeature/web-sdk@^1.7.3 ``` ```ts import { OpenFeature } from "@openfeature/web-sdk"; import { createBasestackWebProvider } from "@basestack/openfeature-provider/web"; export async function startWebFlags( projectKey: string, environmentKey: string, ) { await OpenFeature.setProviderAndWait( createBasestackWebProvider({ projectKey, environmentKey, apiUrl: "https://flags-api.basestack.co/v1", refreshIntervalMs: 30_000, }), ); const client = OpenFeature.getClient("basestack-web"); return client.getBooleanValue("new-checkout", false); } ``` Do not render the enabled experience before readiness. Web defaults are `prefetch: true`, infinite local TTL, and a 30 s full-snapshot refresh interval. Synchronous evaluations only inspect the snapshot; setting `cacheTtlMs: 0` is not a way to fetch synchronously and can make every lookup miss. On refresh failure an older snapshot may remain; infinite TTL can preserve stale values. Choose a reviewed refresh/staleness policy, not a promise of instant changes. Both providers use `apiUrl`, `cacheTtlMs`, `refreshIntervalMs`, `prefetch`, `requestTimeoutMs` (5,000 by default), optional `headers`, and `fetch`. They do not use flags-js's `baseURL` or nested `cache` shape. Close the provider through the OpenFeature lifecycle (`await OpenFeature.close()`) at application shutdown/teardown to clear timers/cache; do not close after every flag read. ## Values, failures, and safety Disabled or expired flags return the caller-provided default. For enabled flags, the provider unwraps `payload.value` when present; otherwise it uses the payload. String/object values must match the requested type; boolean evaluation uses a boolean payload when present or the flag's enabled state otherwise. Use `false` as the boolean default. Missing flags and service/type errors resolve to defaults; inspect details/logging when you need diagnostics, and validate object schemas yourself. These providers do not implement user targeting from evaluation context. Never send a write/API/MCP token to a web provider. Test provider readiness, disabled/default values, malformed payloads, missing flags, failed refresh, cache timing, and cleanup with mocked fetch only. Human docs: [Server provider](https://docs.basestack.co/content/feature-flags/sdks/backend/open-feature-node-sdk), [Web provider](https://docs.basestack.co/content/feature-flags/sdks/frontend/open-feature-web-sdk). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/vercel-flags-sdk.md # Vercel Flags SDK adapter Last reviewed: 2026-10-16. Targets `@basestack/vercel-flags-sdk-adapter@1.0.6` and its `flags ^4.3.0` peer. Use ESM and a fetch-capable server runtime (published Node >=18.17.0). ```sh bun add @basestack/vercel-flags-sdk-adapter@1.0.6 flags@^4.3.0 ``` ## Explicit boolean and payload resolution For Next.js server code, import `flag` from `flags/next`. Initialize an adapter once for a stable project/environment, not once per flag read or across tenants. ```ts import { flag } from "flags/next"; import { createBasestackAdapter } from "@basestack/vercel-flags-sdk-adapter"; export function defineCheckoutFlag(projectKey: string, environmentKey: string) { const adapter = createBasestackAdapter({ projectKey, environmentKey, endpoint: "https://flags-api.basestack.co/v1", cacheTtlMs: 30_000, resolveValue: (document) => document.enabled === true, }); return flag({ key: "new-checkout", adapter, defaultValue: false, }); } export function defineCopyFlag(projectKey: string, environmentKey: string) { const adapter = createBasestackAdapter({ projectKey, environmentKey, resolveValue: (document) => document.enabled && typeof document.payload === "string" ? document.payload : "Continue", }); return flag({ key: "checkout-copy", adapter, defaultValue: "Continue", }); } ``` In the app's server flags module, call `defineCheckoutFlag` once with validated server environment variables and reuse the returned function. In a server component, `await checkoutFlag()` evaluates it. Handle invalid bootstrap config before constructing the adapter; missing keys throw. The default resolver is `document.payload ?? document.enabled`, **not always a boolean** and not automatically gated by `enabled`. Always provide a boolean resolver for boolean flags. For payload configuration, decide deliberately what disabled means and validate its schema; do not blindly cast the payload. Review expiration behavior separately if the app relies on `expiredAt`. ## Options, caching, and fallback The URL option is `endpoint`, the API base including `/v1`, not a `/flags/slug` URL. Other options include `fetch`, `headers`, `requestTimeoutMs`, `cacheTtlMs` (30,000 default; `0` disables local cache), `prefetch: "none" | "all"`, `resolveValue`, and `onError`. `prefetch: "all"` uses the initialization hook. Successful documents are cached per client in memory across evaluations, not only for a single request. API/CDN caching can add further staleness. On a missing document, fetch failure, or resolver exception the adapter returns `undefined` so `flags` can use `defaultValue`. `onError` must not throw; observe errors without leaking keys/payloads. Test defaults with the installed `flags` release, including failed fetch and invalid payloads. Do not assume the adapter adds targeting, percentage allocation, or streaming. `BasestackFlagsClient` is also exported with `getFlag(slug)` and `listFlags()` when raw access is needed. Keep evaluation identifiers separate from private write/API/MCP tokens. Never use feature flags as authorization controls. Human docs: [Vercel Flags SDK](https://docs.basestack.co/content/feature-flags/sdks/backend/vercel-flags-sdk). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/web-components.md # Preview and feedback Web Components Last reviewed: 2026-10-16. Targets `@basestack/flags-wc@1.0.4`, ESM in a browser with custom elements. These are preview/feedback widgets, not a replacement for application evaluation. Use the [JavaScript SDK](https://docs.basestack.co/skills/basestack-feature-flags/references/javascript.md) or [React SDK](https://docs.basestack.co/skills/basestack-feature-flags/references/react.md) to gate application UI. ```sh bun add @basestack/flags-wc@1.0.4 ``` ## Browser-only registration with explicit exports Do not run custom-element registration during SSR. Use the real package export paths, not the obsolete `@example/...` README import. This module can be called from a browser lifecycle hook with real evaluation identifiers: ```ts export async function mountPreview(projectKey: string, environmentKey: string) { if (typeof window === "undefined") return; const [preview, feedback] = await Promise.all([ import("@basestack/flags-wc/components/feature-flag-preview-modal"), import("@basestack/flags-wc/components/feature-flag-feedback-modal"), ]); preview.defineCustomElement(); feedback.defineCustomElement(); const modal = document.createElement("feature-flag-preview-modal"); modal.setAttribute("project-key", projectKey); modal.setAttribute("environment-key", environmentKey); modal.setAttribute( "api-endpoint", "https://flags-api.basestack.co/v1/flags/preview", ); document.body.append(modal); const feedbackModal = document.createElement("feature-flag-feedback-modal"); feedbackModal.setAttribute("project-key", projectKey); feedbackModal.setAttribute("environment-key", environmentKey); feedbackModal.setAttribute("flag-key", "new-checkout"); feedbackModal.setAttribute( "api-endpoint", "https://flags-api.basestack.co/v1/flags/preview/feedback", ); document.body.append(feedbackModal); return () => { modal.remove(); feedbackModal.remove(); }; } ``` The root also exports `registerFeatureFlagComponents`, `fetchFeatureFlags`, `readSelectionState`, `writeSelectionState`, and `toggleFlagState`. Explicit component subpath imports above keep the browser bundler's imports analyzable. Supply explicit endpoints: this release's preview/feedback defaults include older paths and must not be copied into a new integration. ## HTTP and persistence Preview reads use `GET https://flags-api.basestack.co/v1/flags/preview` with `x-project-key` and `x-environment-key`. The JSON envelope is `{ flags: [...] }`. Feedback uses `POST .../flags/preview/feedback`, the same two headers, and `Content-Type: application/json`. The body contains `flagKey`, `mood` (`VERY_SAD`, `SAD`, `NEUTRAL`, `HAPPY`, `VERY_HAPPY`), integer `rating` 1–5, optional `description` (up to 2,000 characters), and optional object `metadata`. Successful feedback returns HTTP 201 with `{ success: true, feedbackId }`. The target must be a preview flag in the selected environment. Selections persist in localStorage under `bs-flags-preview-state` by default (`storage-key` can change the preview widget's key). This is a local opt-in, not a dashboard toggle. Your application must consume the selection state; React's default preview integration reads the default key. Do not assume that changing localStorage updates every running component immediately. Never pass a private API/MCP token or sensitive metadata to these widgets. Review privacy/consent before submitting feedback. Verify registration once, SSR safety, explicit URLs/headers, local storage failures, loading/error UI, and cleanup. Test with mocks; obtain approval before a real feedback write. Human docs: [Web Components](https://docs.basestack.co/content/feature-flags/sdks/frontend/web-components). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/cli.md # CLI code-reference scanning Last reviewed: 2026-10-16. Targets `@basestack/flags-cli@1.0.5`. **Bun is required to run this CLI**, despite the package's generic Node engine field. It scans and uploads code references; it is not a general flag-management CLI and cannot be assumed to create, toggle, or delete flags. ```sh bun add --dev @basestack/flags-cli@1.0.5 ``` ## Consent before upload `code-refs` scans source and sends file paths, line numbers, surrounding source snippets, branch/project information, and repository metadata to Basestack. Require deliberate user opt-in before running it or adding it to CI. Review the destination and a narrowly scoped scan directory; generated code, secrets, and proprietary snippets must not be uploaded inadvertently. This command is an upload, not a dry run. Do not run it to test this documentation. After approval, run the locally installed executable: ```sh bun run --bun flags-cli code-refs --dir=./src ``` Supply `BASESTACK_FLAGS_PROJECT_KEY`, `BASESTACK_FLAGS_ENVIRONMENT_KEY`, and private `BASESTACK_FLAGS_API_KEY` using the shell/CI secret store, not literals in a command or checked-in file. The API key is a project write token, not an account MCP token. Optional `BASESTACK_FLAGS_API_URL` defaults to `https://flags-api.basestack.co/v1`. ## Configuration precedence For credentials and API URL: CLI flags override `.flagsrc` in the scan directory, which overrides environment variables. `--dir` selects the scan directory (default current directory). This matters in CI: a stale `.flagsrc` can override the intended environment's keys. Supported flags: `--project-id` (takes a project **key**), `--environment-key`, `--api-key`, `--api-url`, `--dir`, and `--help` / `-h`. Prefer secret environment variables over `--api-key` so the token is not exposed in shell history/process arguments. `.flagsrc` supports JSON (`projectKey`, `environmentKey`, `apiKey`, `apiUrl`) or env-style entries; if used for credentials, keep it gitignored. ## CI recipe, only after source-upload approval Install with the application's lockfile and configure a trusted CI job with Bun and the three secrets above. Its command is the same scoped local command. Never provide write secrets to untrusted fork jobs, print the environment, or commit a token. Non-interactive execution uses plain output instead of the TUI. Do not treat scanner success as proof all dynamic flag references were found. The upload is `POST /v1/flags/code-refs` with `x-project-key`, `x-environment-key`, and `x-api-key`. Test scan/config behavior locally with mocked requests, not a production upload. Inspect uploaded references in the correct dashboard environment after an approved run. Human docs: [CLI](https://docs.basestack.co/content/feature-flags/sdks/tooling/cli). --- Source: https://docs.basestack.co/skills/basestack-feature-flags/references/rest-api.md # Feature Flags REST reads Last reviewed: 2026-10-16. Contract: Feature Flags API `/v1`, verified against the repository's public routes/middleware on this date. API deployments can lag this source snapshot; confirm self-hosted versions. This is evaluation, not account management or an invented CRUD API. Base: `https://flags-api.basestack.co/v1`. All reads require `x-project-key` and `x-environment-key`; use `Accept: application/json`. Do not substitute a project write token or account MCP token for these keys. - `GET /flags` returns `{ flags: [...] }`. Entries include `slug`, `enabled`, `payload`, `expiredAt`, `description`, `createdAt`, and `updatedAt`. - `GET /flags/{encoded-slug}` returns a single document with `slug`, `enabled`, `payload`, `expiredAt`, and `description`. Do not depend on list-only timestamps. - `GET /flags/preview` returns `{ flags: [...] }` for preview flags, including preview name/content. See [Web Components](https://docs.basestack.co/skills/basestack-feature-flags/references/web-components.md) for that workflow. ## Safe HTTP client This fetch-based example is also a contract template for another language. It performs no request until explicitly called with real keys. ```ts export async function readFlag( projectKey: string, environmentKey: string, slug: string, ) { if (!projectKey || !environmentKey || !slug) return false; try { const response = await fetch( `https://flags-api.basestack.co/v1/flags/${encodeURIComponent(slug)}`, { headers: { Accept: "application/json", "x-project-key": projectKey, "x-environment-key": environmentKey, }, }, ); if (!response.ok) return false; const body: unknown = await response.json(); return ( typeof body === "object" && body !== null && "enabled" in body && body.enabled === true && !("error" in body && body.error) ); } catch { return false; } } ``` Missing headers return 400; project permission failures can be non-2xx with `{ error: true, message }`; a missing single flag returns 404 with `{ enabled: false, error: true, message }`. Rate limiting, proxies, and server failures may return other statuses or non-JSON bodies. Inspect HTTP status and validate JSON, rather than assuming every response is a flag. Parse timestamps explicitly and validate payload schemas before using them. ## Freshness and verification These routes advertise `public, s-maxage=60, max-age=30, stale-while-revalidate=60, stale-if-error=300` and can also use server-side cache. Actual freshness depends on deployment/CDN behavior. Clearing a client cache does not purge upstream caches; no instantaneous kill-switch guarantee follows. Avoid shared caching without project/environment separation when implementing your own proxy. Read evaluation identifiers and browser payloads are public to the consumer; feature flags are not authorization controls. Prefer HTTP when the language lacks an SDK or when controlling transport is important. Use SDKs when their caching/provider semantics fit the app. Test enabled, disabled, missing, malformed JSON/payload, denied project, and service failure with mock responses. Never invent management endpoints from these reads. Human docs: [REST](https://docs.basestack.co/content/feature-flags/sdks/backend/rest-api). --- Source: https://docs.basestack.co/skills/basestack-forms/SKILL.md # Basestack Forms Last reviewed: 2026-10-16. Contract baseline: Forms API `/v1` submission route and delivery workers in the repository on this date. There is no dedicated Forms SDK in this integration inventory. Check the deployed API/self-hosted release when behavior differs; public docs do not grant account access. ## When to use Use for contact forms, lead capture, feedback, or other authorized submissions from an existing application. Standard HTML or HTTP is sufficient; MCP is not required and does not replace frontend integration knowledge. ## Required inputs - Stack/runtime and whether to redirect the browser or remain on the page. - The real form endpoint/key copied from the dashboard, and the test environment. - Field names, configured validation rules, honeypot name, and enabled state. - Redirect URLs, retention, uploads, website/IP restrictions, and delivery needs. - Consent for personal data collection and any forwarding/notification destination. If missing, explain [form creation](https://docs.basestack.co/content/forms/user-guide/forms) and [endpoint setup](https://docs.basestack.co/content/forms/user-guide/setup). Never fabricate a key or create/change account resources without approval. ## Choose an integration - Browser navigation, no JavaScript: [HTML](https://docs.basestack.co/skills/basestack-forms/references/html.md). - JSON/fetch, a server client, or files: [JavaScript/REST](https://docs.basestack.co/skills/basestack-forms/references/javascript-rest.md). - Interactive React or Next.js: [React/Next.js](https://docs.basestack.co/skills/basestack-forms/references/react-nextjs.md), plus [JavaScript/REST](https://docs.basestack.co/skills/basestack-forms/references/javascript-rest.md) for the response contract. - Vue: [Vue](https://docs.basestack.co/skills/basestack-forms/references/vue.md). - Rules, honeypot, privacy, retention, attachments: [validation, security, and files](https://docs.basestack.co/skills/basestack-forms/references/validation-security-files.md). - Webhook POST and configured email notifications: [integrations](https://docs.basestack.co/skills/basestack-forms/references/integrations.md). ## Workflow 1. Inspect existing form/state/validation patterns and confirm the real endpoint. 2. Choose `POST https://forms-api.basestack.co/v1/s/FORM_KEY` for HTML redirects or append `?mode=rest` for JSON response mode. `Accept` alone does not select JSON mode. Replace the named `FORM_KEY` placeholder before use. 3. Use ordinary HTML/HTTP; do not install an invented `@basestack/forms-react` or other unpublished Forms SDK. Use JSON, URL-encoded data, or multipart. 4. Give fields accessible labels and `name` attributes matching server rules. Use a real configured honeypot name, not an assumed magic `_trap` field. 5. Provide pending/success/error states, prevent accidental duplicate clicks, inspect HTTP status and JSON body, and preserve inputs after failure. 6. For files, enable retention and uploads, use `FormData`, and let the browser set the multipart boundary. Current limits: 1 MiB per file, five files total. 7. Review security/privacy and webhook/email settings. Acceptance is not delivery. 8. Add mocked tests. Send a synthetic test to a non-production form only with permission, especially if notifications or forwarding will be triggered. ## Verification Cover success, invalid fields/rules, disabled form, wrong key, honeypot rejection, network failure, malformed/non-JSON error responses, duplicate-click prevention, and file count/size failures. Verify dashboard retention and receiver/email delivery separately after an approved test. Do not report a hypothetical live test as passed or automatically retry an ambiguous submission. ## Safety constraints - The submission endpoint is browser-visible, not a private account credential. Keep private API/MCP tokens, webhook destination credentials, and email service secrets out of frontend code and public Markdown. - Client validation, CORS, website restrictions, and a honeypot do not prevent all abuse or prove identity. Validate fields on the server using configured rules. - Non-string JSON fields are stringified; do not promise nested typed storage. - Native delivery covered here is webhook POST and email notifications only. Google Sheets and native Slack/Discord connectors are unverified/unreleased for this guide. Do not invent setup or blindly forward to incompatible APIs. - Webhooks are asynchronous and can retry/duplicate. The current sender has no signature header or stable event ID; do not invent HMAC or exactly-once delivery. - Obtain consent before forwarding personal data to a new destination or sending tests that trigger email/webhooks. Avoid real personal data in fixtures/logs. - Query-string submission data can leak into history, logs, analytics, or referrers. Keep it disabled for sensitive forms; retention-off does not disable forwarding. Human setup: [Using these files with an AI assistant](https://docs.basestack.co/content/forms/guides/ai-agents). --- Source: https://docs.basestack.co/skills/basestack-forms/references/html.md # HTML forms and redirects Last reviewed: 2026-10-16. Forms API `/v1` submission contract; no SDK needed. Copy the endpoint from the dashboard and replace `FORM_KEY` below. Match each field name to configured validation rules. Standard HTML mode follows a 303 redirect; use [JavaScript/REST](https://docs.basestack.co/skills/basestack-forms/references/javascript-rest.md) to stay on the current page. ```html
``` Without `mode=rest`, successful submissions redirect to the configured success URL (or the default status page). Expected request errors also redirect to an error URL with a message; unexpected failures may instead return JSON/500. HTTP 303 by itself is not proof of success. Configure success/error/go-back URLs in the dashboard; do not invent hidden `_next` fields or query options. ## Honeypot and files If a honeypot is configured, add an empty field with that exact name. For example, **only if the dashboard name is `company_website`**, add: ```html ``` Do not choose a real user field as the honeypot. It is removed from submission data; a nonempty string is rejected. Do not assume a default field name. For attachments, enable retention and file uploads in the dashboard, then add `enctype="multipart/form-data"` to the form and an input such as: ```html ``` Current limits are 1 MiB (1,048,576 bytes) per file and five files per submission, not per input. Ordinary form fields default to URL encoding; files require multipart, not JSON/base64. See [validation/security/files](https://docs.basestack.co/skills/basestack-forms/references/validation-security-files.md) before enabling uploads or query-string data. Verify labels, native required/email validation, configured rule names, redirects, disabled form behavior, wrong keys, and rejection handling. A real submission can send email/webhooks: obtain permission and use synthetic data on a test form. Acceptance does not prove downstream delivery. Human docs: [HTML](https://docs.basestack.co/content/forms/guides/html), [Customization](https://docs.basestack.co/content/forms/user-guide/settings-customization). --- Source: https://docs.basestack.co/skills/basestack-forms/references/javascript-rest.md # JavaScript and REST submissions Last reviewed: 2026-10-16. Targets Forms API `/v1/s/:formId` and its current response envelopes. No Forms SDK or extra HTTP client dependency is required. Use the real dashboard endpoint, replacing `FORM_KEY` before any approved test. ## JSON and response handling `?mode=rest` selects JSON responses; an `Accept` header alone does not. This module exports helpers and makes no network request until called. ```ts export async function acceptedSubmission(response: Response): Promise { let body: unknown; try { body = await response.json(); } catch { throw new Error(`Unexpected server response (HTTP ${response.status}).`); } if ( !response.ok || typeof body !== "object" || body === null || !("success" in body) || body.success !== true ) { // A generic UI error avoids exposing raw provider text or sensitive details. throw new Error(`Submission was not accepted (HTTP ${response.status}).`); } } export async function submitContact( endpoint: string, fields: { name: string; email: string; message: string; }, ): Promise { const url = new URL(endpoint); url.searchParams.set("mode", "rest"); const response = await fetch(url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(fields), }); await acceptedSubmission(response); } export async function submitFiles( endpoint: string, form: HTMLFormElement, ): Promise { const url = new URL(endpoint); url.searchParams.set("mode", "rest"); const response = await fetch(url, { method: "POST", headers: { Accept: "application/json" }, body: new FormData(form), }); await acceptedSubmission(response); } ``` For `submitFiles`, **do not set multipart Content-Type manually**: the browser must include its generated boundary. URL-encoded fields are also supported with `URLSearchParams`. Ensure the form's fields have `name` attributes. Send scalar text fields for predictable rules/storage: JSON null values are omitted and other non-string JSON values (numbers, booleans, objects, arrays) are stringified with `JSON.stringify`, not stored as arbitrary nested typed records. ## Envelopes and common failures Success is HTTP 200: ```json { "code": 200, "success": true, "message": "Your form has been submitted successfully!", "url": "https://forms.basestack.co/status/success?goBackUrl=https://example.com/contact" } ``` The illustrative `url` is server-generated from form settings. Do not hardcode it or navigate to arbitrary response URLs without reviewing the destination. Known request errors use their HTTP status plus `{ error: true, code, message, url }`; unexpected 500s may have only `{ error: true, message }`. Examples of statuses: 400 parsing/rule failures or uploads disabled; 403 honeypot, subscription, or security restrictions; 404 unknown form; 409 disabled form; 413 oversized/too many files. Proxies or rate limiters may return a different/non-JSON envelope. Inspect status **and** body. In UI, set pending before awaiting fetch, disable repeat submits, catch errors, announce a useful recoverable failure, and clear pending in `finally`. Preserve input after failure; clear only after confirmed success. See [React/Next.js](https://docs.basestack.co/skills/basestack-forms/references/react-nextjs.md) or [Vue](https://docs.basestack.co/skills/basestack-forms/references/vue.md) for complete state handling. Do not automatically retry after a timeout: the submission may already have been accepted and there is no documented idempotency key here. ## Verification and privacy Mock fetch for accepted JSON, non-2xx JSON, a 200 without `success: true`, invalid JSON, rejected network requests, and file encoding. Verify current 1 MiB/five-file limits and dashboard retention/upload settings using [validation/security/files](https://docs.basestack.co/skills/basestack-forms/references/validation-security-files.md). An accepted submission is not confirmation of webhook/email delivery. Never send real data or trigger notifications without approval. No private API/MCP token is needed in browser requests; keep account credentials out of frontend code. Human docs: [JavaScript](https://docs.basestack.co/content/forms/guides/javascript), [REST](https://docs.basestack.co/content/forms/guides/rest). --- Source: https://docs.basestack.co/skills/basestack-forms/references/react-nextjs.md # React and Next.js contact form Last reviewed: 2026-10-16. Uses React hooks and the Forms API `/v1` contract, not a Basestack Forms SDK. In Next.js App Router, interactive event handlers belong in a `"use client"` component; in a React SPA the same component works. Pass the real dashboard endpoint as `endpoint`. It is a public submission URL, not a private account credential. ```tsx "use client"; import { useState, type FormEvent } from "react"; export function ContactForm({ endpoint }: { endpoint: string }) { const [pending, setPending] = useState(false); const [status, setStatus] = useState(""); const [error, setError] = useState(""); async function submit(event: FormEvent) { event.preventDefault(); if (pending) return; const form = event.currentTarget; const fields = new FormData(form); setPending(true); setStatus(""); setError(""); try { const url = new URL(endpoint); url.searchParams.set("mode", "rest"); const response = await fetch(url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ name: fields.get("name"), email: fields.get("email"), message: fields.get("message"), }), }); const body: unknown = await response.json(); if ( !response.ok || typeof body !== "object" || body === null || !("success" in body) || body.success !== true ) { throw new Error("Submission was not accepted."); } setStatus("Your message was accepted."); form.reset(); } catch { setError( "We could not confirm submission. Your input is still here. Check before retrying.", ); } finally { setPending(false); } } return (
Contact us

{{ status }}

{{ error }}

``` This uses `FormData` so a configured honeypot field can be included naturally. Do not manually set multipart Content-Type or its boundary. Capture `currentTarget` before awaiting, disable repeat submissions, and preserve inputs on failure. For JSON instead, send explicit scalar fields with `Content-Type: application/json` and `JSON.stringify`; non-string values are normalized to strings by the API. `Accept` alone is insufficient without `mode=rest`. Add file inputs only after enabling retention/uploads and reviewing the 1 MiB per-file / five-file limits. See [validation/security/files](https://docs.basestack.co/skills/basestack-forms/references/validation-security-files.md). Private account/API/MCP tokens never belong in the component or a public Vite variable. A successful response means acceptance, not notification delivery. Test success, disabled form, validation errors, malformed response, rejected fetch, pending state, and input preservation with mocks. Request permission before any real submission or notification. See [JavaScript/REST](https://docs.basestack.co/skills/basestack-forms/references/javascript-rest.md) for envelopes and retries. Human docs: [Vue](https://docs.basestack.co/content/forms/guides/vue). --- Source: https://docs.basestack.co/skills/basestack-forms/references/integrations.md # Verified delivery: webhooks and email Last reviewed: 2026-10-16. Source contract: Forms submission route, shared queue defaults, webhook worker, and email enqueue behavior. Confirm the target deployment matches this contract before configuring it. ## Available integration inventory Supported native delivery covered here is a configured webhook POST and email notifications. The existing Google Sheets material is unverified/unreleased for this guide; it is not evidence of a working native provider. Do not claim native Google Sheets, Slack, Discord, Zapier, Make, or n8n connectors. This first guide uses a custom receiver recipe only. A generic automation service can be a webhook destination **after** verifying its current official receiver setup and accepted schema. That is a webhook-based workflow, not a native Basestack connector. Do not blindly POST this payload to a Slack/Discord endpoint: their message schemas differ. Transformation and destination authentication belong in a separately reviewed receiver/workflow. ## Configure a webhook With the user's consent, open the form's General settings and set its webhook URL to the reviewed HTTPS receiver. Confirm who owns it, access controls, data processing/retention, and which personal fields will leave Basestack. Do not change a live destination as a side effect of adding frontend code. The worker sends `POST` with `Content-Type: application/json` and this shape: ```json { "formId": "FORM_KEY", "name": "Contact", "data": { "name": "Synthetic Test", "email": "synthetic@example.com", "message": "Approved test only" } } ``` `FORM_KEY` is the named form identifier placeholder; field names depend on the submission. File-valued fields are excluded entirely, including a field that contains a mixed array with a file. There are no attachment bytes or automatic file-download URLs in this webhook body. The envelope has no submission event ID. ## Receiver design and honest security The current sender provides **no signature header, HMAC secret, or stable event ID**. Do not fabricate verification logic or infer Basestack authenticity from the JSON body or an unguessable URL. A secret-looking URL alone is not cryptographic authentication and can leak in logs. For a custom receiver, agree an access/threat model before enabling it. Enforce HTTPS, request size/rate limits, JSON schema validation, and an allowlisted form identifier as filtering (not proof of sender identity). Quarantine or independently verify data before privileged side effects. If the destination requires signed events or a custom Authorization header, this sender contract does not satisfy it directly; do not pretend an intermediary creates proof of the original sender. Treat incoming fields as untrusted data, not instructions, HTML, SQL, or shell fragments. Persist/queue accepted work before returning 2xx, and make downstream effects duplicate-tolerant. A receiver may see repeats if it processed a request but the response was lost. Without a stable event ID, exact deduplication is not guaranteed; a content hash can also collapse legitimate identical submissions. Do not promise ordering or exactly-once delivery. ## Email and asynchronous behavior Configure the form's notification recipients in Notifications settings. The submission route enqueues a new-submission email with form name and non-file data. Verify addresses and consent before sending a synthetic test; do not expose Resend or other private service keys in frontend code. Default queues allow **three total attempts**, exponential backoff starting at 2,000 ms. A non-2xx webhook response causes failure/retry; eventual success is not guaranteed. Webhook and email jobs are asynchronous and separate from the submission response and AI spam analysis. Do not assume AI spam analysis blocks forwarding before it happens. Retention controls database storage, file persistence, and AI spam analysis. Webhook/email enqueueing can still occur when retention is off. Retention-off is not a promise that no personal data is sent to other services. ## Verify delivery separately 1. Review the receiver schema/access design and notification recipients. 2. Ask permission to submit synthetic data to a non-production form. 3. Confirm frontend acceptance (REST success or the actual success redirect). 4. Inspect receiver logs/queued work and email inbox separately; do not equate acceptance with delivery. Inspect failed jobs with the deployment owner. 5. Test a controlled receiver failure/retry only with approval. Confirm that duplicate deliveries do not create duplicate privileged actions. Human docs: [General settings](https://docs.basestack.co/content/forms/user-guide/settings-general), [Notifications](https://docs.basestack.co/content/forms/user-guide/settings-notifications). --- Source: https://docs.basestack.co/skills/basestack-forms/references/validation-security-files.md # Validation, security, retention, and attachments Last reviewed: 2026-10-16. Contract: Forms `/v1` submission parsing/verification, rules schema, dashboard settings, and S3 upload constants. Verify self-hosted release/configuration before relying on these limits. ## Field rules and enabled state Copy the real endpoint/key from the dashboard. Field `name` attributes/JSON keys must match configured rule field names exactly; a matching label is insufficient. Configured rules run server-side on non-file values before storage/delivery. Mirror them in the browser for usability, not as the only validation boundary. Test required/format/length rules against the actual saved rule configuration; do not invent unsupported rule options. Non-string JSON values are normalized to strings, nulls are omitted, and repeated multipart names can form arrays. When rules are present, the generated object schema is strict: unexpected non-file field names also fail validation. Include every intended text field in the saved rule inventory, not just the required ones. Supported rule kinds are `required`, `email`, `url`, `number`, `phoneNumber`, `minLength`, `maxLength`, `contains`, and `notContains`. For a `number` field, the length-named bounds are numeric min/max checks; do not describe them as digit-count validation. A disabled form rejects submissions (REST 409). An unknown form returns 404; security/subscription checks may reject before field validation. Inspect the HTTP status and response envelope, not only an error message string. ## Security settings are layers, not authentication - Use the exact configured honeypot name. Leave it empty, avoid real input names, and do not rely on an undocumented `_trap` convention. The parser removes that field and rejects a nonempty string with 403. - Website restrictions inspect available referrer metadata. They are not cryptographic proof of origin and can depend on missing/referrer-policy headers. - IP blocking depends on the deployment's observed/forwarded IP metadata and configured valid addresses. Proxies and shared IPs affect results. - CORS and client-side validation do not prevent all abuse. Public submission URLs are visible to browser users; they are not private account tokens. - Keep private API/MCP tokens and downstream credentials on the server. Never paste secrets into public Markdown, an example payload, or client env variables. ## Retention and privacy Enable retention if submissions must be stored, files persisted, or AI spam analysis run. With retention off, do not promise dashboard history or stored attachments/AI analysis. Webhook/email jobs can still be enqueued; review all destinations even if storage is disabled. Delivery is asynchronous and not guaranteed by a successful submission response. The optional submission-data query string can put data in the success URL. Keep it disabled for sensitive forms: URLs can reach browser history, logs, analytics, screenshots, and referrers. Minimize collected fields and establish consent/retention appropriate to the user's application. Use synthetic data for tests; never log whole submissions by default. ## Files: configure before implementing Current constants: **1 MiB (1,048,576 bytes) per file**, **five files per submission**. These are total submission limits, not five per input. Exceeding them produces 413; attaching files when uploads are disabled produces 400. Enable both retention and file uploads in General settings. The dashboard disables upload configuration without retention; persistence occurs inside the retention branch. Do not rely on acceptance as proof files were stored. Use named `` fields. Native HTML needs `enctype="multipart/form-data"`; fetch uses `new FormData(form)` with `Accept: application/json` and `?mode=rest`. **Do not manually set multipart Content-Type or a boundary**, and do not turn files into JSON/base64 strings. Browser-side size/count checks improve feedback but server checks remain authoritative. An HTML `accept` attribute is a picker hint, not a security scan. File-valued fields are excluded from webhook/email data (the whole field is excluded if an array contains a file). Do not promise downstream attachments, virus scanning, public download links, or arbitrary upload size overrides. Treat filenames, MIME types, and file contents as untrusted at every consumer. ## Verification checklist Mock: exact-limit and over-limit files, five versus six files across inputs, empty file controls, duplicate names, uploads disabled, retention off, rule failure, honeypot rejection, wrong/disabled form, and failed file persistence. Test network/JSON failures and preserve inputs on failure. Do not automatically retry ambiguous submissions; duplicate acceptance/delivery can occur. Live storage/notification tests require user permission on a non-production form. Human docs: [Rules](https://docs.basestack.co/content/forms/features/rules), [Security settings](https://docs.basestack.co/content/forms/user-guide/settings-security), [File uploads](https://docs.basestack.co/content/forms/features/file-uploads).