# ProofKit: webviewer
> FileMaker Web Viewer utilities
Documentation for the @proofkit/webviewer package.
---
# Architecture
URL: https://proofkit.proof.sh/docs/webviewer/architecture
How ProofKit connects agents, local tools, FileMaker, and Web Viewer apps.
import { Callout } from "fumadocs-ui/components/callout";
ProofKit has two related architectures: the development loop that lets an agent build the app, and the runtime loop that lets the deployed Web Viewer app talk to FileMaker.
Development architecture [#development-architecture]
* **MCP**: Model Context Protocol, the tool protocol the coding agent uses to talk to the ProofKit MCP server.
* **Local API**: The local bridge between the ProofKit MCP server and the FileMaker plug-in.
* **Script calls**: FileMaker script execution used by the plug-in and add-on to work with your FileMaker file.
Runtime architecture [#runtime-architecture]
* **ProofKit type safe client**: Generated client code that calls FileMaker's Data API through the Execute Data API script step.
* **fmFetch()**: A lightweight script bridge for calling FileMaker scripts from the Web Viewer app.
How to think about it [#how-to-think-about-it]
* During development, the agent uses ProofKit tools to understand the FileMaker file and deploy bundles.
* At runtime, the deployed web app talks to FileMaker through fmFetch scripts and the type-safe Data API path.
* FileMaker scripts remain the secure place for privileged operations, external API secrets, filesystem work, and print/PDF flows.
Related pages [#related-pages]
* [Agent Workflow](/docs/ai/agent-workflow)
* [Runtime Under the Hood](/docs/webviewer/runtime-under-the-hood)
* [FileMaker Scripts as Backend](/docs/webviewer/filemaker-scripts-as-backend)
---
# callFMScript
URL: https://proofkit.proof.sh/docs/webviewer/callFmScript
Call a FileMaker script
If you want to simply call a FileMaker script and you don't need any data back, you can use the `callFMScript` helper function.
```ts
import { callFMScript } from "@proofkit/webviewer";
```
Then call the function like so
```ts
callFMScript("scriptName", { param1: "value1", param2: "value2" });
```
The script parameter is passed as the second parameter and can be a JSON object or a string. The parameter will automatically be stringified on its way to FileMaker.
Calling Script with Options [#calling-script-with-options]
You can optionally pass a third parameter to specify how the script should run. The options are:
* Continue
* Halt
* Exit
* Resume
* Pause
* Suspend and Resume
As a helper method, you can import these values from the package and reference them by name:
```ts
import { callFMScript, FMScriptOption } from "@proofkit/webviewer";
FMScriptOption.CONTINUE; // 0
FMScriptOption.HALT; // 1
// etc...
callFMScript("scriptName", {}, FMScriptOption.RESUME);
```
See the [Claris documentation](https://help.claris.com/en/pro-help/content/options-for-starting-scripts.html) for more details about each option.
---
# Run JS from FileMaker
URL: https://proofkit.proof.sh/docs/webviewer/commands
How to use the Perform JavaScript in Web Viewer script step to trigger functions inside your web viewer app.
FileMaker can call a global Web Viewer namespace. Your app registers typed handlers on that namespace.
Initialize [#initialize]
Call `initWebViewerCommands()` once in browser code.
```ts
import { initWebViewerCommands } from "@proofkit/webviewer/commands";
initWebViewerCommands();
```
The module is import-safe on the server. `initWebViewerCommands()` returns `undefined` when `window` is unavailable.
Type commands [#type-commands]
Declare commands in a `.d.ts` file. Extending `DefineWebViewerCommandRegistry` makes TypeScript check that every command is a function that accepts only string parameters.
```ts
import type { DefineWebViewerCommandRegistry } from "@proofkit/webviewer/commands";
export {};
declare module "@proofkit/webviewer/commands" {
interface WebViewerCommandRegistry
extends DefineWebViewerCommandRegistry<{
openCustomer: (recordId: string) => void;
refreshDashboard: () => void;
}> {}
}
```
FileMaker passes string parameters, so command parameters must be strings.
You can also wrap individual entries with `WebViewerCommandHandler`.
```ts
import type { WebViewerCommandHandler } from "@proofkit/webviewer/commands";
declare module "@proofkit/webviewer/commands" {
interface WebViewerCommandRegistry {
openCustomer: WebViewerCommandHandler<(recordId: string) => void>;
}
}
```
Static commands [#static-commands]
Use `registerWebViewerCommand` for handlers that do not depend on component state. This example lets FileMaker ask the Web Viewer to load a customer snapshot through `fmFetch`.
```ts
import { fmFetch } from "@proofkit/webviewer";
import { registerWebViewerCommand } from "@proofkit/webviewer/commands";
registerWebViewerCommand("loadCustomerSummary", async (recordId) => {
const summary = await fmFetch("Load Customer Summary", { recordId });
window.dispatchEvent(
new CustomEvent("customer-summary-loaded", {
detail: summary,
})
);
});
```
React commands [#react-commands]
Use `useWebViewerCommand` when a handler depends on UI state, routing, or component data.
```tsx
import { useWebViewerCommand } from "@proofkit/webviewer/react";
export function CustomerScreen() {
useWebViewerCommand("openCustomer", (recordId) => {
console.log(recordId);
});
return null;
}
```
The hook registers on mount, unregisters on unmount, and keeps the latest callback after rerenders.
Next.js [#nextjs]
Initialize from Client Components only.
```tsx
"use client";
import { initWebViewerCommands } from "@proofkit/webviewer/commands";
import { useEffect } from "react";
export function WebViewerCommandBootstrap() {
useEffect(() => {
initWebViewerCommands();
}, []);
return null;
}
```
Use import-time initialization only in client-only modules.
```tsx
"use client";
import { initWebViewerCommands } from "@proofkit/webviewer/commands";
initWebViewerCommands();
```
Early calls [#early-calls]
Missing commands buffer by default. If FileMaker calls before React mounts, the call replays when the command registers.
```ts
initWebViewerCommands({
missingCommand: "buffer",
maxBufferedCallsPerCommand: 100,
});
```
Other missing-command modes are `drop`, `warn`, and `throw`.
Migration [#migration]
Avoid direct `window.proofkit` assignment. It bypasses buffering, cleanup, and type checks.
Before:
```tsx
useEffect(() => {
window.proofkit = {
openDialog: () => setOpen(true),
};
}, []);
```
After:
```tsx
useWebViewerCommand("openDialog", () => setOpen(true));
```
---
# Data Access
URL: https://proofkit.proof.sh/docs/webviewer/data-access
How ProofKit Web Viewer apps read and write FileMaker data.
For hybrid apps, the generated data path uses FileMaker's Execute Data API script step, called through FileMaker scripts from the Web Viewer app.
The basic flow [#the-basic-flow]
1. The React app needs data for a screen.
2. TanStack Query calls a ProofKit data function.
3. The Web Viewer bridge calls a FileMaker script.
4. The script runs Execute Data API against a layout.
5. FileMaker returns JSON to the Web Viewer.
6. The UI renders and caches the result.
Custom scripts for reads and writes [#custom-scripts-for-reads-and-writes]
Execute Data API is the default generated path, not the only way to read or write FileMaker data. A Web Viewer app can call any FileMaker script that accepts a JSON parameter and returns JSON to the app.
Use a custom script when a screen needs data or behavior that is awkward to express as one layout query or simple generated write, such as:
* Aggregated reporting data.
* Data from multiple layouts or files.
* A response shaped specifically for one workflow.
* Transactions.
* Validation before destructive actions.
* Multi-step workflows.
* Permission-aware behavior that depends on FileMaker session context.
* Results from external APIs, plug-ins, file system access, or other FileMaker-only capabilities.
For the bridge contract, keep the boundary simple: send JSON in and return JSON out. Read [FileMaker Scripts as Backend](/docs/webviewer/filemaker-scripts-as-backend) for the higher-level pattern, or [`fmFetch`](/docs/webviewer/fmFetch) for the lower-level script call and callback mechanics.
Layouts define the data shape [#layouts-define-the-data-shape]
Execute Data API reads from FileMaker layouts, so the fields available to the web app are the fields available through those layouts.
Good layout design makes better Web Viewer apps:
* Use focused layouts for single entities like companies, contacts, or invoices.
* Use composite layouts when a screen needs one primary record plus related portal data.
* Keep heavy calculated or unstored fields out of broad list queries when possible.
*An example layout designed for API access. It uses relationships to fetch an invoice, its related line items, and related customer data in a single request, which is often the most efficient shape for a detail screen.*
Type safety [#type-safety]
The [@proofkit/typegen](/docs/typegen) CLI reads FileMaker metadata and generates TypeScript types and validators. That gives the agent field names, record shapes, and compile-time feedback while it builds the app.
Whenever FileMaker layouts or fields change, make sure you re-run the `npx @proofkit/typegen` command to keep your types up to date. [Learn more](/docs/typegen).
Full web apps [#full-web-apps]
For full browser apps hosted outside FileMaker, use server-oriented transports such as the FileMaker Data API or OData. See [@proofkit/fmdapi](/docs/fmdapi) and [@proofkit/fmodata](/docs/fmodata) for package details.
---
# Deployment Methods
URL: https://proofkit.proof.sh/docs/webviewer/deployment-methods
Learn about the different methods for deploying Web Viewer code to your FileMaker file.
There are many ways to deploy Web Viewer code to your FileMaker file, and each method has it's own pros and cons. This document attempts to outline the various options so you can make the best choice for your app.
Embedded [#embedded]
This technique stores the Web Viewer code as **data** directly in your FileMaker file. This is the default method for most Web Viewer integrations and the method used when setting up a new project with ProofKit.
This is also the default supported deployment model in ProofKit. The main AI guides and built-in project flow assume the embedded method unless a page explicitly says otherwise.
| Pros | Cons |
| -------------------------------- | -------------------------------------------------------------------------- |
| ✅ Simple to setup and understand | ⚠️ Doesn't survive data migrations |
| ✅ Works offline | ⚠️ Unable to use server-side JavaScript libraries |
| | ⚠️ Updates requires the web developer to have access to the FileMaker file |
Considerations [#considerations]
If it weren't for the data migration issues, the embedded method would easily be the best choice. It fits our mental model so well about what we expect when developing a FileMaker solution with everything being part of the same FileMaker file. If you aren't doing data migrations, this is a great choice. But if are working in a seperate development envionrment (which we strongly recommend), you simply need to understand additional steps required.
To get around the data migration issue, you have a few options:
**Use a sidecar/utility file for the Web Viewer code.** Essentially, any file that you will always **copy or replace** when doing a migration. Some solutions already have a UI or Interface file that serves this purpose and you can just use that.
**Use a migration-only utility file.** If you need to keep your users in single open file in their FileMaker Pro client, or don't want to load the Web Viewer code from a seperate file, you can essentially create a mirror of the required Web Viewer table in a utlity file and use a [post-deployment script](https://docs.ottofms.com/concepts/deployments/deployment-scripts) to copy the code from the utility file to a table in the main file.
**Store the Web Viewer code in a script** This method is generally not recommended because it's not possbile to automate the updaing process whenever you make changes to the web code, but it can be used to store the code as **schema** in the file which will survive data migrations. This option may only work for simple widgets, as you might run into FileMaker's character limits.
Hosted [#hosted]
In the other extreme, you can host the Web Viewer code like you would any other web application. If you're only using the @proofkit/webviewer library, you can still do this without any security concerns because the data won't load unless the web page is loaded in your FileMaker solution in a Web Viewer.
| Pros | Cons |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| ✅ Code survives data migrations | ⚠️ Requires a web server, or hosting account with Vercel (or similar) |
| ✅ Can use server-side JavaScript libraries | ⚠️ Code is de-coupled from the FileMaker file, which may cause schema to be out of sync if you're not careful |
| ✅ Can deploy updates to the web code without a data migration | ⚠️ Requires a persistent internet connection |
Implementation [#implementation]
To implement this method with a ProofKit-initialized Web Viewer project, you'll need to make the following changes:
* Remove the `viteSingleFile` plugin from your `vite.config.ts` file and package.json file
* This plugin is not needed when deploying it as a standard web app, and will cause performance issues when loaded from a server
* Remove the `upload` and `build:upload` scripts in your package.json file
* *(optional)* Remove the `HashHistory` override for the router in the `main.tsx` file so that your URLs behave more like a traditional web app
* Deploy the code to a host like Vercel. Vercel will automatically detect that this is a Vite project and build it as you expect.
* Edit the Web Viewer object to load from your production URL (from Vercel) instead of the `ProofKitWV::HTML` field
* You may also want to add more steps to the case statement to load different URLs based on the environment, such as `development`, `staging`, and `production`
Alternatively, you can just install the `@proofkit/webviewer` library into a standard Next.js web app if you're going to use the Hosted method.
Downloaded [#downloaded]
This technique is a hybrid of the embedded and hosted methods. Essentially, the code is embedded in the FileMaker file, but you also host a copy of it on another web server. The FileMaker file (or the web code) can check for updates and download the latest version directly to the the neccesary field.
| Pros | Cons |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| ✅ Enables a self-updating solution | ⚠️ Requires a server to host a copy of the code (but can be a simple static file host or CDN) |
| ✅ Great for non-hosted files, or a vertical-market solution where each copy may need a different version of the Web Viewer code | ⚠️ More complex to setup and maintain |
| ✅ Can work offline, after the initial download | |
Comparison Table [#comparison-table]
| Feature | Embedded | Hosted | Downloaded |
| ------------------------------------ | ---------------------------- | ------------------------ | -------------------------------- |
| **Simple Setup** | ✅ | ⚠️ (needs hosting) | ⚠️ (hybrid setup) |
| **Survives Data Migrations** | ❌ | ✅ | ✅ |
| **Works Offline** | ✅ | ❌ | ✅ (after download) |
| **Web Server Required?** | No | Yes | Simple (static file host or CDN) |
| **Can Use Server-side JS Libraries** | ❌ | ✅ | ❌ |
| **Update Method** | ⚠️ File migration (downtime) | ✅ Simplest (no downtime) | ✅ Self-updating, no downtime |
---
# FileMaker Scripts as Backend
URL: https://proofkit.proof.sh/docs/webviewer/filemaker-scripts-as-backend
Use FileMaker scripts for secure business logic behind Web Viewer apps.
FileMaker scripts are the backend for many hybrid app workflows. The Web Viewer handles the interface, while scripts perform privileged work inside FileMaker.
When to use a script [#when-to-use-a-script]
Use a custom FileMaker script when the operation needs:
* Business rules that already live in FileMaker.
* Validation before creating, updating, or deleting records.
* Transaction-style workflows.
* External API calls with secrets that should not be in browser code.
* File system access.
* Printing, PDF generation, or container handling.
Request and response pattern [#request-and-response-pattern]
Keep the bridge contract simple: send JSON in, return JSON out.
```json title="Request"
{
"action": "approveInvoice",
"invoiceId": "123",
"notes": "Approved from Web Viewer"
}
```
```json title="Response"
{
"ok": true,
"invoiceId": "123",
"status": "Approved"
}
```
If an operation fails, return a structured error the web app can display.
```json title="Error response"
{
"ok": false,
"error": {
"code": "VALIDATION_FAILED",
"message": "This invoice cannot be approved until it has at least one line item."
}
}
```
Script templates included with the add-on [#script-templates-included-with-the-add-on]
The ProofKit add-on includes two sample scripts that already follow the callback pattern used by `fmFetch`:
* `FETCH CALLBACK TEMPLATE`
* `FETCH CALLBACK TEMPLATE (Server version)`
Copy one of these scripts when you need a FileMaker-backed endpoint for your Web Viewer. The regular version is for scripts that can safely run in the user's current FileMaker session. The server version is for workflows that should run on FileMaker Server, such as longer-running work or tasks that need a clean server context.
Both templates follow the same structure:
* Read the incoming JSON from `Get ( ScriptParameter )`.
* Pull out the `callback` value that tells FileMaker where to send the response.
* Optionally read `data` for the request payload sent from the Web Viewer.
* Set `$webViewerName` to the Web Viewer object name that should receive the callback.
* Build `$result` as a JSON object.
* Call the included `SendCallBack` script with the callback, result, and Web Viewer name.
Replace only the middle "update info here" block with your business logic. Leave the setup and final callback steps in place so the Web Viewer receives the response that `fmFetch` is waiting for.
*The add-on's `FETCH CALLBACK TEMPLATE` shows the required pattern: read the request JSON, build a result object, and send that result back to the Web Viewer callback.*
Why scripts are powerful here [#why-scripts-are-powerful-here]
Scripts run inside the FileMaker security and data model. That means the web app can ask FileMaker to do work without moving privileged logic, credentials, or platform-specific behavior into browser JavaScript.
Related docs [#related-docs]
* [Runtime Under the Hood](/docs/webviewer/runtime-under-the-hood)
* [Data Access](/docs/webviewer/data-access)
* [Web Viewer package docs](/docs/webviewer/package)
---
# fmBridge
URL: https://proofkit.proof.sh/docs/webviewer/fm-bridge
Load your local development server in any browser as if it were in a FileMaker Web Viewer.
import { Callout } from "fumadocs-ui/components/callout";
`fmBridge` lets your local dev server talk to a real FileMaker file. With it running, code that calls `window.FileMaker` or uses `@proofkit/webviewer` works the same in `pnpm dev` as it does inside a deployed Web Viewer: no rebuild, no upload, no mock data.
Why it matters [#why-it-matters]
A Web Viewer app expects to live inside FileMaker. Outside of FileMaker, `window.FileMaker` does not exist and any script call throws. Without a bridge, the only way to test the real integration is to build, upload, and reopen the file every time you change a line of code.
FM Bridge removes that loop:
* Forwards `window.FileMaker.PerformScript*` calls from the browser to a connected FileMaker file over WebSocket.
* Auto-discovers the connected file from the local FM MCP server, so you don't need to hard-code a file name.
* Lets you keep using your normal dev workflow while still hitting real FileMaker scripts and data.
Setup [#setup]
The Web Viewer project generated by the [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) already wires up the Vite version. If you are configuring a project by hand, use one of the setups below.
Vite [#vite]
```ts
import { fmBridge } from "@proofkit/webviewer/vite-plugins";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [fmBridge()],
});
```
Next.js App Router [#nextjs-app-router]
Use the Next component directly in your root `app/layout.tsx`.
```tsx
import { FmBridgeScript } from "@proofkit/webviewer/nextjs";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
Next.js Pages Router [#nextjs-pages-router]
Pages Router still needs a small server-side resolve step in `pages/_document.tsx`, because `_document` is server-only and `beforeInteractive` scripts must be attached there. Resolve in `getInitialProps`, then render the provided component.
```tsx
import Document, {
Head,
Html,
Main,
NextScript,
type DocumentContext,
type DocumentInitialProps,
} from "next/document";
import {
ResolvedFmBridgeScript,
getFmBridgeScriptProps,
type NextFmBridgeScriptProps,
} from "@proofkit/webviewer/nextjs";
interface Props extends DocumentInitialProps {
fmBridgeScript: NextFmBridgeScriptProps | null;
}
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext): Promise {
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
fmBridgeScript: await getFmBridgeScriptProps(),
};
}
render() {
return (
);
}
}
```
Options [#options]
```tsx
```
* `fileName` — pin the bridge to a specific FileMaker file. Useful when more than one file is connected.
* `fmMcpBaseUrl` — base URL of the local FM MCP daemon. Defaults to `http://localhost:1365`.
* `wsUrl` — WebSocket endpoint for script forwarding. Computed from `fmMcpBaseUrl` if not set.
* `debug` — log bridge activity to the browser console.
The Next.js bridge is development-only, like the Vite plugin. In production it renders nothing.
How to use it [#how-to-use-it]
1. Start the FM MCP daemon locally.
2. Open the FileMaker file you want to develop against.
3. Run the **Connect to MCP** script in that file. It opens a small Web Viewer window that registers the file with FM MCP and proxies script calls.
4. Run your dev server (e.g. `pnpm dev`).
From the web app, call FileMaker scripts the same way you would in production:
```ts
import { fmFetch } from "@proofkit/webviewer";
const result = await fmFetch("My Script", { id: 123 });
```
The bridge discovers the connected file via `GET /connectedFiles` on FM MCP, opens a WebSocket to `/ws`, and forwards each script call to FileMaker.
The FileMaker **Connect to MCP** script opens a Web Viewer window that the bridge depends on. It must stay open and in **Browse mode** while you develop — closing the window or switching the file to Layout mode silently breaks the bridge.
Troubleshooting [#troubleshooting]
* **"fmBridge could not reach ..."** — FM MCP is not running or is bound to a different port. Check that the daemon is up and that `fmMcpBaseUrl` matches.
* **"fmBridge found no connected FileMaker files"** — A FileMaker file is open but has not registered with FM MCP. Run **Connect to MCP** in that file again.
* **Script calls hang or fail at runtime** — The connector Web Viewer was closed or the file was put into Layout mode. Reopen the connector window.
---
# fmFetch
URL: https://proofkit.proof.sh/docs/webviewer/fmFetch
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
import { Callout } from "fumadocs-ui/components/callout";
The purpose of the fmFetch function is to call a FileMaker script and get the result of the FileMaker script back to your web application running in a webviewer. If you don't care about the script result, check out the [callFMScript](/docs/webviewer/callFmScript) function instead.
To accomplish this, this function wraps the `FileMaker.PerformScript` function injected into the webviewer by FileMaker Pro and assigns each invocation of your fetch with a unique ID. In turn, the FileMaker script that you call must call back into the webviewer with this callback ID and the result of your script.
To see a working example of this, download this [demo file](/fmFetch-demo.fmp12).
Simple Example [#simple-example]
Let's say you have the following in your Javascript code:
```ts title="index.ts"
import { fmFetch } from "@proofkit/webviewer";
async function getData() {
const result = await fmFetch("GetSimpleResult");
}
```
And the following in your FileMaker script named `GetSimpleResult`:
```FileMaker title="GetSimpleResult"
# Required properties
Set Variable [ $json ; Value: Get ( ScriptParameter ) ]
Set Variable [ $callback ; Value: JSONGetElement ( $json ; "callback" ) ]
Set Variable [ $webViewerName ; "web" ]
# $result must be an object.
Set Variable [ $result ; Value: JSONSetElement ( "" ; [ "hello" ; "world" ; JSONString ] ) ]
Set Variable [ $callback ; Value: JSONSetElement ( $callback ; ["result"; $result; JSONObject ]; ["webViewerName"; $webViewerName; JSONString ]) ]
Perform Script [ Specified: From list ; "SendCallBack" ; Parameter: $callback ]
```
The `SendCallBack` script comes with the FileMaker addon that you installed
with the [ProofKit CLI](/docs/cli), or from the [Demo
File](/fmFetch-demo.fmp12).
The awaited result of the `fmFetch` call will be:
```json
{
"hello": "world"
}
```
Passing Script Parameters [#passing-script-parameters]
A script parameter can be passed to the fmFetch function as a string or JS object. The script parameter will be passed to the FileMaker script as a string.
```ts
import { fmFetch } from "@proofkit/webviewer";
async function getData() {
const result = await fmFetch("ScriptName", {
param1: "value1",
param2: "value2",
});
}
```
Then simply parse out the script parameter via the data key in your FileMaker script.
```FileMaker title="ScriptName"
Set Variable [ $json ; Value: Get ( ScriptParameter ) ]
Set Variable [ $callback ; Value: JSONGetElement ( $json ; "callback" ) ]
Set Variable [ $data ; Value: JSONGetElement ( $json ; "data" ) ]
```
TypeScript Support [#typescript-support]
If you want to directly type the result of your FileMaker script, you can pass a type to the fmFetch function.
```ts
type Result = {
hello: string;
};
async function getData() {
const result = await fmFetch("GetSimpleResult");
}
```
The type that you pass here is not validated with the actual FileMaker script
result. You may want to consider validating the data that is returned from
FileMaker with a runtime validation library such as [zod](https://zod.dev).
This technique is most powerful when combined with the Execute FileMaker Data
API script step and automatic type generation found in the
[@proofkit/fmdapi](/docs/fmdapi) package.
---
# Data API Integration
URL: https://proofkit.proof.sh/docs/webviewer/fmdapi
import { Callout } from "fumadocs-ui/components/callout";
import { Steps, Step } from "fumadocs-ui/components/steps";
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
We can use the `Execute FileMaker Data API` script step to harness the power of the [@proofkit/fmdapi](/docs/fmdapi) library in our webviewer integration.
* ✅ Use the same code and functions as in a browsed-based app
* including typegen for a nice auto-complete experience and runtime validation for protection if field names are changed.
* ✅ No authentication required (it runs in the process of the logged in user)
* ✅ Works in offline FileMaker apps
* ✅ Works even if the Data API is disabled on the server
Setup [#setup]
If you followed the [ProofKit AI build workflow](/docs/ai/build-a-webviewer-app),
these steps are already done for you.
Install both packages [#install-both-packages]
npm
pnpm
yarn
bun
```bash
npm install @proofkit/fmdapi @proofkit/webviewer
```
```bash
pnpm add @proofkit/fmdapi @proofkit/webviewer
```
```bash
yarn add @proofkit/fmdapi @proofkit/webviewer
```
```bash
bun add @proofkit/fmdapi @proofkit/webviewer
```
FileMaker Script Installation [#filemaker-script-installation]
Copy the `ExecuteDataAPI` and `SendCallback` scripts from the [demo file](/fmdapi-demo.fmp12) to your own FileMaker solution.
Initialize the DataAPI client [#initialize-the-dataapi-client]
For more details about this step, see the [@proofkit/fmdapi](/docs/fmdapi) documentation.
If you're using using [typegen](/docs/fmdapi/typegen), modify your `fmschema.config.mjs` file to include the script name that calls the `Execute FileMaker Data API` script step
```js title="fmschema.config.mjs"
export const config = {
// ...other config
webviewerScriptName: "ExecuteDataApi",
};
```
Then simply run the typegen command to generate the client.
If you're manually creating the client, use the webviewer adapter from the `@proofkit/webviewer` package.
```ts title="client.ts"
import { DataApi } from "@proofkit/fmdapi";
import { WebViewerAdapter } from "@proofkit/webviewer/adapter";
export const client = DataApi({
adapter: new WebViewerAdapter({ scriptName: "ExecuteDataApi" }),
layout: "API_Customers", // put your layout name here
});
```
Repeat this for each layout that you want to interact with.
Usage [#usage]
Now you can use the DataAPI client just as you would in a browsed-based app!
```ts
import { UsersClient } from "./schema/client";
const users = await UsersClient.findOne({ query: { id: "===1234" } });
```
For examples of all methods, see the [@proofkit/fmdapi](/docs/fmdapi) documentation.
---
# Web Viewer Apps
URL: https://proofkit.proof.sh/docs/webviewer
The hybrid app model and the @proofkit/webviewer bridge.
import { Card, Cards } from "fumadocs-ui/components/card";
import { Callout } from "fumadocs-ui/components/callout";
A ProofKit Web Viewer app is a modern web interface running inside a FileMaker Web Viewer. The web layer handles the UI. FileMaker still provides the data, security, scripts, file system access, printing, and deployment environment.
This section covers both the conceptual model and the `@proofkit/webviewer` bridge package that connects the web app to FileMaker scripts and data.
Start with the [ProofKit AI guide](/docs/ai/getting-started) for the recommended install, build, and deploy path. Use this Web Viewer section when you want to understand the hybrid app model or work directly with the bridge package.
Learn why a Web Viewer app can be a practical architecture, not just an escape hatch.
See how the agent, MCP server, plug-in, add-on, FileMaker file, and Web Viewer connect.
Understand loading, communication, caching, saving, record locking, containers, and printing.
Use the bridge to call FileMaker scripts and read data from the web app.
The short version [#the-short-version]
Think of the Web Viewer as the interface layer and FileMaker as the application platform underneath it.
* The user interacts with React UI in the Web Viewer.
* The web app calls FileMaker scripts when it needs data or business logic.
* Scripts read and write the database with the user's FileMaker permissions.
* Scripts can also call external APIs, access files, generate PDFs, and use plug-ins.
Start with [Why WebViewers?](/docs/webviewer/why-webviewers) for the concept, then read [Data Access](/docs/webviewer/data-access) when you are designing real screens.
---
# Initial Props
URL: https://proofkit.proof.sh/docs/webviewer/initial-props
Pull bootstrap data from FileMaker into the Web Viewer at startup.
import { Callout } from "fumadocs-ui/components/callout";
The **initial props** pattern is how a Web Viewer app gets bootstrap data from FileMaker at startup — things like the current user, the active record ID, or a starting route.
Problems you might be solving [#problems-you-might-be-solving]
FileMaker and the Web Viewer are separate runtimes with no shared memory. The web app cannot read FileMaker fields or globals directly, and FileMaker cannot safely inject values before the JavaScript has loaded. Initial props bridge that gap.
* **Your web app needs to know the current user at startup** — account name, display name, or privilege set so the UI can adapt before the first render.
* **You want the Web Viewer to open to a specific screen** depending on which FileMaker layout, record, or button launched it.
* **You need the active record ID before the first render** so the app can immediately fetch or display the right data.
* **Data you inject into the Web Viewer HTML keeps getting lost** — substitution or script-trigger approaches race against bundle loading and silently fail.
If any of these sound familiar, read on. The rest of this page shows the pattern and gives copy-ready examples.
Pull, don't push [#pull-dont-push]
The Web Viewer asks FileMaker for its initial props. FileMaker does not push them in.
Concretely: the Web Viewer calls a FileMaker script via [`fmFetch`](/docs/webviewer/fmFetch) once it has mounted, and uses the returned data to finish bootstrapping.
Do **not** seed initial props by:
* Substituting values into the HTML / web viewer URL before render.
* Calling `FileMaker.PerformJavaScriptInWebViewer` from an `OnLayoutEnter` or `OnRecordLoad` script trigger when the viewer opens.
Both paths race against the web app loading. The script step or substitution can fire before your JavaScript bundle has parsed and your handler is attached, and the props are silently lost.
The pull direction inverts the timing problem. By the time the Web Viewer makes the `fmFetch` call, it has confirmed three things at once:
1. The JavaScript bundle has loaded and executed.
2. `window.FileMaker` has been injected by Pro / Go and is callable.
3. Any handlers the FileMaker script needs to call back into (via the `SendCallBack` script) are wired up.
In the ProofKit Web Viewer template, the `fmFetch` call fires the moment the web code loads — **before** the router mounts and **before** the first route renders. The router then receives the resolved props as part of its context, so the first screen a user sees can already depend on FileMaker state (the signed-in user, the active record, a starting route) without a flash of empty UI or a post-mount re-render.
Basic shape [#basic-shape]
```ts title="bootstrap.ts"
import { fmFetch } from "@proofkit/webviewer";
import { z } from "zod";
const initialPropsSchema = z.object({
user: z.object({
id: z.string(),
name: z.string(),
email: z.email(),
}),
initialRoute: z.string().optional(),
});
type InitialProps = z.infer;
export async function getInitialProps(): Promise {
const result = await fmFetch("GetInitialProps");
return initialPropsSchema.parse(result);
}
```
On the FileMaker side, `GetInitialProps` collects whatever the app needs and sends it back through the standard `fmFetch` callback (see [fmFetch](/docs/webviewer/fmFetch) for the script shape).
Validate the script result with [zod](https://zod.dev) (or similar). The Web
Viewer cannot trust shape inference across the FileMaker boundary.
Example: initial route [#example-initial-route]
A Web Viewer that uses a client-side router (e.g. TanStack Router with hash history) can be told where to start. Useful when one FileMaker layout hosts a viewer that should land on different screens depending on context.
```ts title="src/router.ts"
import { fmFetch } from "@proofkit/webviewer";
import { createHashHistory, createRouter } from "@tanstack/react-router";
import { z } from "zod";
import { routeTree } from "./route-tree";
const initialPropsSchema = z.object({
initialRoute: z.string().optional(),
});
const GET_INITIAL_PROPS_SCRIPT = "GetInitialProps";
export async function createAppRouter() {
const result = await fmFetch(GET_INITIAL_PROPS_SCRIPT);
const { initialRoute } = initialPropsSchema.parse(result);
if (initialRoute && !window.location.hash) {
window.location.hash = initialRoute;
}
return createRouter({
history: createHashHistory(),
routeTree,
});
}
```
```FileMaker title="GetInitialProps"
Set Variable [ $json ; Value: Get ( ScriptParameter ) ]
Set Variable [ $callback ; Value: JSONGetElement ( $json ; "callback" ) ]
# Pick a starting route based on whatever FileMaker context matters.
If [ not IsEmpty ( Customers::id ) ]
Set Variable [ $route ; Value: "/customers/" & Customers::id ]
Else
Set Variable [ $route ; Value: "/" ]
End If
Set Variable [ $result ; Value: JSONSetElement ( "" ;
[ "initialRoute" ; $route ; JSONString ]
) ]
Set Variable [ $callback ; Value: JSONSetElement ( $callback ;
[ "result" ; $result ; JSONObject ] ;
[ "webViewerName" ; "web" ; JSONString ]
) ]
Perform Script [ Specified: From list ; "SendCallBack" ; Parameter: $callback ]
```
The check on `window.location.hash` matters: if the user has already navigated inside the viewer, you should not yank them back to the initial route on a refresh.
Example: current user [#example-current-user]
Hand the app the user identity it needs to render.
```ts title="src/lib/initial-props.ts"
import { fmFetch } from "@proofkit/webviewer";
import { z } from "zod";
export const initialPropsSchema = z.object({
user: z.object({
accountName: z.string(),
fullName: z.string(),
privilegeSet: z.string(),
}),
});
export type InitialProps = z.infer;
export async function fetchInitialProps(): Promise {
const result = await fmFetch("GetInitialProps");
return initialPropsSchema.parse(result);
}
```
```FileMaker title="GetInitialProps"
Set Variable [ $json ; Value: Get ( ScriptParameter ) ]
Set Variable [ $callback ; Value: JSONGetElement ( $json ; "callback" ) ]
Set Variable [ $result ; Value: JSONSetElement ( "" ;
[ "user.accountName" ; Get ( AccountName ) ; JSONString ] ;
[ "user.fullName" ; Get ( UserName ) ; JSONString ] ;
[ "user.privilegeSet" ; Get ( AccountPrivilegeSetName ) ; JSONString ]
) ]
Set Variable [ $callback ; Value: JSONSetElement ( $callback ;
[ "result" ; $result ; JSONObject ] ;
[ "webViewerName" ; "web" ; JSONString ]
) ]
Perform Script [ Specified: From list ; "SendCallBack" ; Parameter: $callback ]
```
In the app, gate render on the bootstrap:
```tsx title="src/main.tsx"
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./app";
import { fetchInitialProps } from "./lib/initial-props";
const propsPromise = fetchInitialProps();
function Boot() {
const props = React.use(propsPromise);
return ;
}
ReactDOM.createRoot(document.querySelector("#root")!).render(
Loading…}>
,
);
```
When initial props are not the right tool [#when-initial-props-are-not-the-right-tool]
Initial props are for **bootstrap**. They are fetched once. If a value can change while the viewer is open (the active record, a selected portal row, a setting toggled elsewhere in the file), prefer:
* An explicit refresh via [`fmFetch`](/docs/webviewer/fmFetch) triggered by a user action.
* A FileMaker-initiated push via `FileMaker.PerformJavaScriptInWebViewer` once you know the viewer is ready — e.g. after the initial-props handshake has completed.
---
# @proofkit/webviewer
URL: https://proofkit.proof.sh/docs/webviewer/package
Use the Web Viewer bridge package directly in custom apps.
import { Callout } from "fumadocs-ui/components/callout";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
Use this page when you already have a Vite app, Next.js app, custom Web Viewer project, or another web app where you want to use the ProofKit bridge directly. If you are new to ProofKit and want the recommended path from install to deployed FileMaker app, start with the [ProofKit AI guide](/docs/ai/getting-started).
What this package does [#what-this-package-does]
`@proofkit/webviewer` makes it easier to work with FileMaker scripts and data from a custom Web Viewer integration. It runs in a FileMaker Web Viewer and lets your app interact with your FileMaker solution through local scripts.
The happy path starts with [ProofKit AI](/docs/ai/getting-started). Come back here when a guide points you to lower-level Web Viewer bridge details, or when you are wiring the package into an existing app yourself.
This is a **client-side** package, meant to run specifically in a FileMaker Web Viewer, but it can still be used in a hosted web app, such as Next.js. However, it will cause errors if loaded in a standard browser. For more information about deployment strategies, see the [Deployment Methods](/docs/webviewer/deployment-methods) guide.
For local development outside FileMaker, use [`fmBridge`](/docs/webviewer/fm-bridge). It supports:
* Vite through `@proofkit/webviewer/vite-plugins`
* Next.js App Router through ``
* Next.js Pages Router through `getFmBridgeScriptProps()` plus ``
For web-based applications where you're looking to interact with the Data API using a network request, check out the [@proofkit/fmdapi](/docs/fmdapi) package instead.
Add it to an existing app [#add-it-to-an-existing-app]
The [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) can scaffold a full Web Viewer project and install the FileMaker add-on that provides the necessary layouts, scripts, and custom functions. Use that path when you want ProofKit to create the project structure for you.
When you scaffold a Web Viewer project with `proofkit init`, ProofKit downloads the latest FileMaker add-on from the ProofKit CDN and opens it in FileMaker automatically; you still need to add the add-on into your FileMaker file. For manual installation, follow the steps below.
{" "}
This demo file is a very simplified example. To see more features, use the
[ProofKit AI workflow](/docs/ai/build-a-webviewer-app) to build a new app or
scaffold one with `proofkit init`, then install the FileMaker add-on.
Use your preferred package manager to install the package.
npm
pnpm
yarn
bun
```bash
npm install @proofkit/webviewer
```
```bash
pnpm add @proofkit/webviewer
```
```bash
yarn add @proofkit/webviewer
```
```bash
bun add @proofkit/webviewer
```
FileMaker File Setup [#filemaker-file-setup]
Download this [Demo FileMaker file](/fmFetch-demo.fmp12) and copy the scripts folder named `fm-webviewer-fetch` into your own FileMaker solution.
Demo file credentials: `admin` / `admin`
Related reference [#related-reference]
* [`fmFetch`](/docs/webviewer/fmFetch) wraps FileMaker script calls that need a result back in the Web Viewer.
* [`callFMScript`](/docs/webviewer/callFmScript) calls a FileMaker script when you do not need a result.
* [`fmBridge`](/docs/webviewer/fm-bridge) lets a local Vite or Next.js dev server talk to a real connected FileMaker file.
* [Data Access](/docs/webviewer/data-access) explains the generated FileMaker data path used by ProofKit Web Viewer apps.
---
# Platform Notes
URL: https://proofkit.proof.sh/docs/webviewer/platform-notes
Runtime notes for FileMaker Pro, FileMaker Go, and WebDirect.
ProofKit Web Viewer apps are built during development and then deployed into FileMaker. After deployment, the runtime environment is FileMaker.
FileMaker Pro [#filemaker-pro]
FileMaker Pro is required for the agentic development workflow. Your file must be open in FileMaker Pro so ProofKit can connect through the local bridge.
Deployed Web Viewer apps can also run in FileMaker Pro on macOS and Windows.
FileMaker Go [#filemaker-go]
After deployment, Web Viewer apps can run in FileMaker Go. Test mobile-specific layouts, viewport behavior, and any script behavior that depends on platform capabilities.
WebDirect [#webdirect]
Deployed Web Viewer apps can run in WebDirect, but browser refresh and session behavior need special care. Avoid designs that assume a user can freely refresh a WebDirect window without affecting their FileMaker session.
End users [#end-users]
End users do not need ProofKit installed. They need the FileMaker runtime where the Web Viewer app is deployed.
Development versus runtime [#development-versus-runtime]
| Requirement | Development | Runtime |
| -------------------------- | ------------ | --------- |
| ProofKit installer | Yes | No |
| FileMaker Pro | Yes | Optional |
| Node.js | Yes | No |
| MCP-compatible agent | Yes | No |
| Deployed Web Viewer bundle | Created here | Runs here |
---
# Runtime Under the Hood
URL: https://proofkit.proof.sh/docs/webviewer/runtime-under-the-hood
What happens when a user opens and interacts with a ProofKit hybrid app.
This page explains the runtime behavior of a deployed ProofKit Web Viewer app: loading, communication, data fetching, caching, saving, and FileMaker-specific behavior.
Loading the app [#loading-the-app]
When a user opens a FileMaker layout with a ProofKit Web Viewer, the Web Viewer loads the deployed HTML and JavaScript bundle from the FileMaker file.
Initial props [#initial-props]
Many apps start by calling a FileMaker script before rendering the full UI. That script can return user context, configuration, permissions, or startup data the web app needs.
This is optional, but it is a good pattern when the app needs FileMaker context before showing a meaningful screen.
Communication bridge [#communication-bridge]
The Web Viewer and FileMaker are separate runtimes. They communicate by passing JSON through FileMaker's script bridge:
* The web app calls FileMaker.
* A FileMaker script performs work.
* FileMaker returns structured data to the Web Viewer.
Under the hood, that bridge uses two FileMaker features:
* JavaScript in the Web Viewer can call `FileMaker.PerformScriptWithOption(script, parameter, option)` to ask FileMaker to run a script. Claris documents this on [Scripting with JavaScript in web viewers](https://help.claris.com/en/pro-help/content/scripting-javascript-in-web-viewers.html).
* FileMaker can call back into the Web Viewer with the [Perform JavaScript in Web Viewer](https://help.claris.com/en/pro-help/content/perform-javascript-in-web-viewer.html) script step.
Those APIs are asynchronous and string-based, so the raw version usually involves passing JSON, tracking callbacks, and deciding how a currently running FileMaker script should behave when JavaScript starts another one.
ProofKit libraries hide most of that callback plumbing so app code can use modern async patterns. [`fmFetch`](/docs/webviewer/fmFetch) wraps the Web Viewer-to-FileMaker call and callback cycle. [`@proofkit/fmdapi`](/docs/fmdapi) and [`@proofkit/typegen`](/docs/typegen) build on FileMaker's data APIs and layout metadata so generated clients can read and write strongly typed FileMaker data without every app reimplementing the bridge details.
Caching and reactivity [#caching-and-reactivity]
ProofKit apps use TanStack Query for server state. In this context, server state means FileMaker state: the data in your FileMaker tables and the behavior exposed through your FileMaker scripts. You can think of those scripts and databases as the server-side piece, even when the app is running in a FileMaker file that is not hosted on a server.
TanStack Query is a React data-fetching library that handles caching, background refreshes, request deduping, and update synchronization for data that lives outside the browser. See the [TanStack Query overview](https://tanstack.com/query/latest/docs/framework/react/overview) for more detail.
Data can be cached locally in the browser, refreshed in the background, and invalidated after writes so the UI stays responsive without manually reloading every screen.
Saving data [#saving-data]
Web Viewer apps usually use explicit save and cancel actions. They do not automatically commit records when a user exits a field the way native FileMaker layouts can.
When saves need business logic, validation, or transactional behavior, send the payload to a FileMaker script and let the script perform the write.
Record locking [#record-locking]
Typing into a Web Viewer form does not hold a FileMaker record lock. This is normal web app behavior, but it is different from editing directly in a FileMaker field.
Common strategies include:
* Check modification counts before saving.
* Use script-mediated saves for transaction-sensitive changes.
* Alert the user when a record changed since it was loaded.
Container data [#container-data]
Data passed through the Web Viewer bridge is JSON. Container data usually needs to be Base64-encoded before it is included in a JSON payload.
Printing and PDFs [#printing-and-pdfs]
For print or PDF workflows, the web app can generate an image or document payload, pass it to FileMaker, and let FileMaker use its native printing or Save as PDF capabilities.
Next step [#next-step]
Read [Data Access](/docs/webviewer/data-access) for practical layout and Execute Data API guidance.
---
# Troubleshooting
URL: https://proofkit.proof.sh/docs/webviewer/troubleshooting
The fmFetch promise is never resolved / callback function is never called / "timed out" error [#the-fmfetch-promise-is-never-resolved--callback-function-is-never-called--timed-out-error]
This happens if the result of your FileMaker script is unable to get it back to the web viewer for one of many reasons:
* The `SendCallback` script is not called at the end of your FileMaker script.
* The `SendCallback` script is called when the user is no longer on the layout that contains the webviewer.
* This most often happens if the user has multiple windows open, or if you are using Perform Script on Server with Callback and the callback resolves in another window, but can also happen if your script simply navigates away and does not return to the webviewer layout before calling `SendCallback`.
* The `SendCallback` script is called with the wrong webviewer name / your webviewer object does not have a name
If the `SendCallback` script is not called, or called with the wrong webviewer name, FileMaker cannot reach back into the JavaScript code to complete the loop. Make sure that you are not exiting/halting the script early or leaving the layout that contains the webviewer.
The FileMaker script does not run. [#the-filemaker-script-does-not-run]
Verify this by opening the script debugger before performing the action in your webviewer that should trigger the script; you should see your script begin executing.
If you try to call a script that does not exist in your FileMaker solution, you will see a FileMaker error dialog. If you don't see that dialog, make sure that the Allow JavaScript to perform FileMaker scripts option is enabled in the webviewer configuration; it is disabled by default.
Error: 'window.FileMaker' was not available at the time this function was called. [#error-windowfilemaker-was-not-available-at-the-time-this-function-was-called]
FileMaker injects neccesary code into the webviewer to enable these interactions, but it is not available immediately when the webviewer first loads. There are many techniques to handle this, but essentially you need to introduce a delay before you use these functions if you run into this error.
---
# Why WebViewers?
URL: https://proofkit.proof.sh/docs/webviewer/why-webviewers
Why modern web UI inside FileMaker is a strong architecture.
A Web Viewer app lets you replace a FileMaker layout with modern web UI while keeping the FileMaker capabilities that already make the system valuable.
What you gain [#what-you-gain]
* **UI freedom**: use modern components, charts, grids, calendars, drag-and-drop, and responsive layouts.
* **Inherited security**: keep FileMaker accounts, privilege sets, and data access rules.
* **Scripts as a backend**: run business logic in FileMaker instead of exposing secrets in browser code.
* **CORS-free requests**: let FileMaker scripts call external APIs when the browser cannot.
* **File system access**: use FileMaker and plug-ins for local machine capabilities.
* **Printing and PDFs**: keep using FileMaker's print and PDF workflows.
* **Staged adoption**: replace one workflow before you rewrite anything else.
A useful analogy [#a-useful-analogy]
Slack, VS Code, and Figma all use web UI in a native shell. A FileMaker hybrid app uses the same broad idea, but the shell includes a multi-user database, security model, scripting engine, and deployment path.
What stays FileMaker [#what-stays-filemaker]
The Web Viewer does not replace your whole solution. Your users, data, security, scripts, and deployment model can remain FileMaker-native while selected screens become modern web experiences.
Next step [#next-step]
Read [Architecture](/docs/webviewer/architecture) to see how the pieces connect.