# Agent Workflow
URL: https://proofkit.proof.sh/docs/ai/agent-workflow
How ProofKit closes the loop for coding agents building FileMaker apps.
import { AgentWorkflowLoop } from "@/components/AgentWorkflowLoop";
ProofKit is designed for agents that can do more than produce snippets. The workflow gives the agent real FileMaker context, a project it can edit, and feedback it can use to fix mistakes.
## The loop
## What each step needs
* **Next change**: the user asks for a new screen, behavior, or refinement.
* **Read FileMaker schema**: the MCP server exposes file metadata and data context to the agent.
* **Write app code**: the agent edits a normal web project using React, TypeScript, Tailwind, and shadcn/ui.
* **Run checks**: TypeGen and TypeScript catch many schema and field-name mistakes before runtime.
* **Preview in browser**: the agent can inspect the app as it runs, not just reason about code.
* **Fix issues**: errors, screenshots, and build output become feedback for the next edit.
* **Deploy to FileMaker**: the final bundle is pushed into the file and loaded by the Web Viewer.
## Why this matters
Without the loop, you are usually copying FileMaker metadata into a chat, pasting code back into a project, discovering errors manually, and asking the model to try again. ProofKit gives the agent enough context and feedback to handle more of that cycle directly.
## Boundaries
ProofKit v2 focuses on agentic coding of Web Viewer apps. It does not make the agent a full FileMaker schema editor. Scripts, tables, fields, layouts, and value lists remain outside the supported writing scope for this release.
## Related pages
* [Install and Connect](/docs/ai/install-and-connect)
* [Build a Web Viewer App](/docs/ai/build-a-webviewer-app)
* [Hybrid App Architecture](/docs/webviewer/architecture)
---
# Build a Web Viewer App
URL: https://proofkit.proof.sh/docs/ai/build-a-webviewer-app
Scaffold and iterate on a FileMaker Web Viewer app with your coding agent.
import { Step, Steps } from "fumadocs-ui/components/steps";
After ProofKit is connected and your agent understands the file, switch from exploration to building. The goal is a local web project that previews in the browser and can later be deployed into FileMaker.
## Build loop
**Ask the agent to scaffold a Web Viewer project.**
Open your coding agent, such as Cursor, Claude Code, Codex, OpenCode, or another MCP-compatible agent, in an empty project folder before you start.
Copy this prompt into your agent:
```text title="Prompt"
Set up a ProofKit Web Viewer project in this folder.
```
The project should use the current ProofKit stack: React, TypeScript, Vite, Tailwind, shadcn/ui, TanStack Query, and TypeGen.
**Run the local preview.**
The agent should start the dev server and open the app in a browser so it can inspect the result. Agents also have embedded browsers, so they can inspect the result without leaving the agentic loop. If the agent doesn't open a browser, ask it to use it's preview browser.
```text title="Prompt"
Use your embedded browser or preview browser to inspect and monitor your work.
```
**Ask for a specific screen.**
Start with one workflow: a dashboard, detail view, calendar, kanban board, rich form, or data grid.
```text title="Prompt"
Build a customer viewer with a searchable list view.
```
*After you ask the agent to build a screen, the desktop session can look like this: agent messages on the left and an embedded browser preview on the right, showing FileMaker data from the connected FileMaker file.*
**Let the agent iterate.**
The useful loop is not just code generation. The agent should typecheck, run lint/build commands, inspect browser errors, and fix issues.
## Style and theme
ProofKit projects use shadcn/ui and Tailwind, so the agent can apply a theme or preset by editing normal project files.
Use [shadcn/ui Create](https://ui.shadcn.com/create?) to explore visual presets. Copy a preset code, then ask your agent to apply it to the project to quickly change colors, typography, icons, and component styling.
```text title="Prompt"
Apply this shadcn/ui preset to the project:
```
*The same generated screen after applying a different preset, with updated colors, typography, and corner radiuses.*
## Reference docs
* Use [Install and Connect](/docs/ai/install-and-connect) for project setup details.
* Use [TypeGen](/docs/typegen) when your FileMaker layout metadata changes.
* Use [Web Viewer](/docs/webviewer/package) for lower-level bridge behavior.
* Use [Hybrid App Data Access](/docs/webviewer/data-access) for layout and Execute Data API patterns.
## Next step
When the app works locally, continue to [Deploy to FileMaker](/docs/ai/deploy-to-filemaker).
---
# Chat Mode vs Code Mode
URL: https://proofkit.proof.sh/docs/ai/chat-vs-code-mode
Understand when to use ProofKit for FileMaker exploration and when to use it to build Web Viewer apps.
ProofKit is useful in two different AI workflows: chat-first exploration and agentic app development.
Both use the same FileMaker connection, but they are not trying to do the same job.
## Chat mode
Chat mode is best when you want to talk to your FileMaker file.
In a chat-first tool such as Claude Desktop or ChatGPT, ProofKit can expose FileMaker context to the model. The assistant can read metadata, inspect layouts and fields, understand relationships, run read-only queries, summarize records, and help you reason about the shape of your data.
This is useful even if you never build a Web Viewer app.
Good chat-mode prompts include:
```text title="Prompt"
Summarize the tables, layouts, and relationships in this FileMaker file.
```
```text title="Prompt"
Show me a table of the last ten invoices in the system.
```
```text title="Prompt"
Show me an ERD diagram for this FileMaker file.
```
**Works with Claude Desktop Chat**
```text title="Prompt"
Use the visualizer to show recent invoice revenue by month.
```
Some chat tools also include built-in visualization features. For example, Claude can turn queried FileMaker data into charts, summaries, and dashboard-style artifacts directly inside the conversation.
Chat mode is good for:
* Exploring schema and relationships.
* Asking questions about FileMaker data.
* Creating summaries, reports, and charts.
* Prototyping dashboard ideas before building.
* Deciding what Web Viewer app would be worth making.
Chat mode is not the full ProofKit app-building loop. The assistant may understand your file, but it usually is not editing a project, running TypeScript checks, previewing a browser app, or deploying a bundle back into FileMaker.
## Code mode
Code mode is for building software.
In a coding agent such as Cursor, Claude Code, Codex, OpenCode, or another MCP-compatible development agent, ProofKit gives the agent FileMaker context while it works inside a real project folder.
That means the agent can use your FileMaker metadata and data while also editing files, installing project dependencies, running checks, inspecting browser output, fixing errors, and deploying the finished app into FileMaker.
Good code-mode prompts include:
```text title="Prompt"
Set up a ProofKit Web Viewer project in this folder.
```
```text title="Prompt"
Build a customer dashboard using the Contacts layout.
```
```text title="Prompt"
Run the app locally, inspect it in the browser, and fix any errors.
```
```text title="Prompt"
Build and deploy this Web Viewer app into my FileMaker file.
```
Code mode is good for:
* Scaffolding a ProofKit Web Viewer project.
* Building React, TypeScript, Tailwind, and shadcn/ui screens.
* Using generated FileMaker types.
* Running lint, typecheck, and build commands.
* Previewing and debugging the app.
* Deploying the bundle into FileMaker.
## How to choose
Use chat mode when your goal is understanding, analysis, reporting, or visualization inside the conversation.
Use code mode when your goal is a working app: project files, local preview, browser testing, and deployment into FileMaker.
A common workflow is to start in chat mode, ask questions about the FileMaker file, identify a useful screen or dashboard, then move to code mode to build it as a real Web Viewer app.
## Related pages
* [Explore Your File](/docs/ai/explore-your-file)
* [Build a Web Viewer App](/docs/ai/build-a-webviewer-app)
* [Agent Workflow](/docs/ai/agent-workflow)
---
# Deploy to FileMaker
URL: https://proofkit.proof.sh/docs/ai/deploy-to-filemaker
Bundle your Web Viewer app and deploy it into a FileMaker file.
import { Step, Steps } from "fumadocs-ui/components/steps";
Deployment for a ProofKit Web Viewer app means bundling the web app and storing it in your FileMaker file so it can load inside a Web Viewer layout.
This is different from deploying a browser app to a hosting provider. The deployed artifact is HTML and JavaScript living in the FileMaker file.
## Ask the agent to deploy
Once the app works locally and FileMaker is connected to ProofKit, you can ask the agent to handle deployment for you:
```text title="Prompt"
Deploy the app to my connected FileMaker file.
```
The agent should treat deployment as a release step, not just a file copy. It should run available checks, build the production bundle, deploy the generated HTML through ProofKit, and then verify the result inside FileMaker.
Because `deploy_html` writes the bundled app into the connected FileMaker file, only run it against the file you mean to update. If users are connected to a production file, they may see the change immediately, so prefer a development file or a controlled deployment window. Most MCP clients let you disable tools or toggle automatic prompting for individual tools; keep deployment approval on unless you are intentionally working in a safe dev environment.
## Deploy the app
When you ask the agent to deploy the app, it should run through these steps for you. The final verification step is the one you should review yourself inside FileMaker.
**Run checks before deployment.**
The agent runs the project checks it has available, such as typechecking, linting, and a production build.
**Bundle the Web Viewer app.**
The agent builds the project into a deployable single-file bundle for the FileMaker Web Viewer.
**Deploy through ProofKit.**
The agent uses ProofKit to send the bundle through the local bridge and store it in the FileMaker file.
**Verify inside FileMaker.**
This step is for you: navigate to the Web Viewer layout and confirm the deployed app loads with real data.
*A deployed ProofKit app running inside FileMaker, with FileMaker's layout chrome around the Web Viewer and live FileMaker data rendered in the dashboard.*
## End-user requirements
Users of the deployed app do not need ProofKit installed. ProofKit is needed during development. The deployed app runs inside FileMaker.
## Next step
Continue with [Edit your app](/docs/ai/edit-your-app) for source vs bundled Web Viewer HTML and how to redeploy updates.
Then read [Agent Workflow](/docs/ai/agent-workflow) to understand how deployment fits into the larger read, write, verify, and fix loop.
---
# Edit your app
URL: https://proofkit.proof.sh/docs/ai/edit-your-app
Work on Web Viewer source in your project, then rebuild and redeploy the bundle into FileMaker.
After you deploy, you will iterate on **source code**, not inside the Web Viewer HTML stored in FileMaker.
## Where your code actually lives
* **Your project folder (on disk, usually in Git)** is the source of truth. React components, TypeScript files, Tailwind styles, and build configuration all live here. Your coding agent and editor work against this folder.
* **Inside FileMaker** you have a **built bundle**: the single-file HTML (and embedded assets) that ProofKit writes when you deploy. That artifact runs in the Web Viewer; it is the output of a build, not a comfortable place to hand-edit layouts or scripts.
If you are used to building everything directly in FileMaker, it helps to separate these two mentally: FileMaker hosts the runtime bundle; your machine hosts the editable app.
## The change loop
1. Open the **same Web Viewer project** in your coding agent (the folder created when you scaffolded the app).
2. **Ask for changes** — new screens, UX tweaks, data wiring, styling. Use the **local preview** so you see updates before touching FileMaker, as described in [Build a Web Viewer App](/docs/ai/build-a-webviewer-app).
3. When you are satisfied, **build and redeploy** so FileMaker receives a fresh bundle, as in [Deploy to FileMaker](/docs/ai/deploy-to-filemaker). Redeployment replaces the stored artifact users load in the Web Viewer.
Changing FileMaker layouts, scripts, or fields may require **regenerating types** — see [TypeGen](/docs/typegen) when metadata changes.
For how agents iterate with checks and verification, see [Agent Workflow](/docs/ai/agent-workflow).
---
# Explore Your File
URL: https://proofkit.proof.sh/docs/ai/explore-your-file
Use chat mode to understand your FileMaker schema before building.
import { Callout } from "fumadocs-ui/components/callout";
Before you build a Web Viewer app, use your agent to explore the FileMaker file. This gives you immediate value from ProofKit and confirms that the agent has useful context.
This page focuses on the exploratory side of ProofKit. To understand how chat-first exploration differs from building an app in a coding agent, see [Chat Mode vs Code Mode](/docs/ai/chat-vs-code-mode).
## Useful first prompts
```text title="Prompt"
Summarize the tables, layouts, and relationships in this FileMaker file.
```
```text title="Prompt"
Show me a table of the last ten invoices in the system.
```
```text title="Prompt"
Show me an ERD diagram for this FileMaker file.
```
**Works with Claude Desktop Chat**
```text title="Prompt"
Use the visualizer to show recent invoice revenue by month.
```
## Example: ProofKit ERD tool
*Claude Desktop can use ProofKit to inspect a connected FileMaker file, render an ERD diagram, and summarize the relationships it finds.*
## What the agent can learn
* Layout and field names.
* Table occurrence and relationship context.
* Script names and the likely workflows they support.
* Data shapes the future Web Viewer app can use.
## Keep exploration grounded
Ask the agent to cite the FileMaker layouts and fields it is using. That keeps ideas tied to real schema instead of generic app suggestions.
ProofKit v2 helps agents understand your FileMaker file and build Web Viewer apps. It does not agentically edit tables, fields, layouts, scripts, or value lists in this release.
## Next step
When you know the screen you want to build, continue to [Build a Web Viewer App](/docs/ai/build-a-webviewer-app).
---
# FAQ
URL: https://proofkit.proof.sh/docs/ai/faq
Common questions about ProofKit AI and v2.
## What is ProofKit?
ProofKit is a toolbox for building FileMaker-backed applications with web technology and AI coding agents. It gives the agent access to FileMaker context, project scaffolding, type-safe data tools, Web Viewer communication, and deployment into your file.
## Is ProofKit a framework?
No. It is a collection of tools. A scaffolded ProofKit app is a normal web project using familiar tools such as React, TypeScript, Tailwind, shadcn/ui, and TanStack Query.
## What does ProofKit produce?
For the v2 launch path, ProofKit focuses on Web Viewer apps: modern web interfaces that run inside FileMaker. The broader toolchain also supports full web apps backed by FileMaker.
## What does ProofKit cost?
ProofKit is free to download and use. The apps you build are yours. The cost you still pay is whatever your AI coding agent or model provider charges.
## Does ProofKit support other models, including DeepSeek models?
Yes. ProofKit is an MCP server, so it is not tied to one model provider. If your coding agent supports MCP, ProofKit can work with it, and that agent can use whatever model or model mix you choose, including DeepSeek models.
## Do my users need ProofKit installed?
No. ProofKit is a development tool. Once the app is bundled and deployed into FileMaker, users only need the FileMaker runtime environment where the app is deployed.
## Can I control the styling?
Yes. ProofKit apps use normal web styling tools: Tailwind, shadcn/ui, React components, and CSS variables. You or your agent can change the theme, layout, and components in the project.
## What happens when the agent writes bad code?
ProofKit gives the agent feedback: TypeScript checks, generated types from FileMaker metadata, browser preview, runtime errors, and deployment verification. That does not make agents perfect, but it gives them the information they need to fix many mistakes without constant manual copying and pasting.
## Can ProofKit edit FileMaker scripts, tables, or layouts?
Not in the supported v2 release scope. ProofKit helps agents build Web Viewer app code. FileMaker scripts, tables, layouts, value lists, and schema design remain outside the supported agentic writing scope.
## Does ProofKit work in WebDirect?
Deployed Web Viewer apps can run in WebDirect, but there are platform-specific considerations around refresh behavior and runtime context. See [Platform Notes](/docs/webviewer/platform-notes).
## How does team workflow work?
For the web project, use normal web development practices: Git, branches, pull requests, CI, and code review. For the FileMaker file, ProofKit does not replace FileMaker's existing shared development model.
## Where do I get help?
Join the [ProofKit community](https://community.proof.sh/c/proofkit) to ask questions, share what you build, and get unstuck.
---
# Getting Started
URL: https://proofkit.proof.sh/docs/ai/getting-started
ProofKit AI path from install through deploy, editing source, and redeploying updates.
import { Step, Steps } from "fumadocs-ui/components/steps";
This page is the overview for the ProofKit v2 happy path. Use it to understand the sequence: install ProofKit and connect MCP, explore your FileMaker file with your agent, build a Web Viewer app, deploy it into FileMaker, then keep working in your source project and redeploy when ready.
Each step below maps to **one guide**: install and connect (single walkthrough), explore, build, deploy, and editing after deploy — with detailed instructions and companion videos where available.
## Before you start
Ensure you have the following prerequisites installed on your computer before installing ProofKit.
* FileMaker Pro (client for Mac or Windows)
* MCP-compatible coding agent (e.g. [Cursor](https://cursor.com), [Claude Desktop](https://claude.com/download), [Codex](https://chatgpt.com/codex/), etc.)
* [Node.js](https://nodejs.org/en/download/) (a supported LTS release, such as 22.x, 24.x, or 26.x)
* [pnpm](https://pnpm.io/) (version 11 is recommended; it is currently the most secure package manager option)
* Git (source control tool for managing the web code your agent will produce)
If you are using a chat-first tool such as Claude Desktop or ChatGPT, you can still use ProofKit to explore FileMaker metadata and data. Building Web Viewer apps requires a coding agent that can work in a project folder. See [Chat Mode vs Code Mode](/docs/ai/chat-vs-code-mode) for the difference.
See [Technical Requirements](/docs/ai/technical-requirements) for the full support matrix.
## Setup
**Install ProofKit, connect MCP, and verify your file.**
Run the installer and add-on, run **Connect to MCP** in FileMaker Pro, keep the connector Web Viewer open in Browse mode, and ask your agent to confirm it sees your open file.
Continue with [Install and Connect](/docs/ai/install-and-connect) (covers install through verification).
**Explore your file in chat mode.**
Ask your agent about tables, layouts, fields, relationships, and data. This proves the connection is useful before you ask it to build anything. For a deeper explanation of exploration versus app development, see [Chat Mode vs Code Mode](/docs/ai/chat-vs-code-mode).
Continue with [Explore Your File](/docs/ai/explore-your-file).
**Build a Web Viewer project.**
Scaffold a project, run a local preview, ask your agent to build a real screen, and let the agent iterate with type checks and browser feedback.
Continue with [Build a Web Viewer App](/docs/ai/build-a-webviewer-app).
**Deploy into FileMaker.**
Bundle the app, deploy it into your FileMaker file, and verify it runs inside the Web Viewer layout.
Continue with [Deploy to FileMaker](/docs/ai/deploy-to-filemaker).
**Edit source and redeploy.**
Iterate on your project on disk — that is where your React and TypeScript source lives — then build and redeploy the bundle stored in FileMaker whenever you want users to pick up changes.
Continue with [Edit your app](/docs/ai/edit-your-app).
## What to read next
* [Install and Connect](/docs/ai/install-and-connect) covers installer, MCP connection, and verification.
* [Edit your app](/docs/ai/edit-your-app) separates source projects from bundles stored in FileMaker.
* [Agent Workflow](/docs/ai/agent-workflow) explains the read, write, verify, and fix loop.
* [Hybrid Apps](/docs/webviewer) explains the runtime model.
* [FAQ](/docs/ai/faq) answers common scope, cost, and platform questions.
---
# GitHub Workflow
URL: https://proofkit.proof.sh/docs/ai/github-workflow
A friendly introduction to Git and GitHub for FileMaker developers.
import { Step, Steps } from "fumadocs-ui/components/steps";
If you've spent your career in FileMaker, Git can feel intimidating. The good news: you don't need to memorize commands or become a Git expert. Your AI agent can run the commands for you. What you *do* need is a mental model of where your code lives and how it moves around. That's what this guide is for.
When you create a new ProofKit project, Git is initialized for you — there's nothing to configure inside the project itself.
If you're completely new to Git, you may want to ask your agent to help you configure Git on your machine. This sets things like your name and email address, which get automatically attached to every commit so the version history knows who made each change.
## The model you already know
In FileMaker, your file lives on a server. You and your teammates open it, make changes, and everyone sees those changes immediately. There's only ever one copy that matters: the hosted file.
This model is simple because there's nothing to sync. The file is in one place, and editing *is* saving. There's also no real history — once a change is made, the previous version is gone unless someone happened to take a backup.
## How code projects work
Web code projects work differently. Instead of one shared file on a server, every developer has their own complete copy of the project on their own machine. That copy is just a folder, but in Git terms it's called a **repo** (short for repository).
You make changes in your local repo, and when you're ready, you push those changes up to a service like **GitHub**. GitHub holds the canonical copy that everyone else syncs with.
The big shift: there is no live shared file. Each developer works in their own copy, and Git is the tool that keeps those copies in sync.
## Terms you'll run into
You don't need to learn these all at once, but it helps to recognize them when your agent (or a teammate) uses them:
* **Repo** — the project folder, tracked by Git. You'll have a local one on your machine and a remote one on GitHub.
* **Clone** — make a local copy of a remote repo on your machine.
* **Commit** — a saved snapshot of your changes, with a short message describing what you did. Think of it as a deliberate "save point" you can always return to.
* **Push** — send your local commits up to GitHub so others can see them.
* **Pull** — bring down commits that teammates have pushed, so your local copy is up to date.
* **Branch** — a parallel line of work. You can experiment on a branch without affecting the main version of the project.
* **Merge** — combine the changes from one branch into another, usually merging a finished branch back into the main one.
* **Pull request (PR)** — a proposal on GitHub to merge one branch into another, usually with a teammate reviewing the changes first.
* **Conflict** — what happens when two people change the same lines of the same file. Git asks you to decide which version wins.
If any of these come up and you want a deeper explanation in the context of your own project, just ask your agent.
## What this gives you
Once your project lives on GitHub, you get things FileMaker developers have long wished for:
* **Full history.** Every change is recorded with who made it, when, and why.
* **Branches.** Try out a risky change in a separate branch without touching the main version. Throw it away if it doesn't work, or merge it in if it does.
* **Pull requests.** Have a teammate review your changes before they become part of the main project.
* **Safe collaboration.** Two developers can work on the same project at the same time without overwriting each other.
## Let your agent do the heavy lifting
You don't need to memorize Git commands. When you want to do something, just ask your agent in plain language:
```text title="Commit your changes"
Commit my current changes with a clear message.
```
```text title="Create a branch"
Create a new branch called fix-customer-list and switch to it.
```
```text title="Push to GitHub"
Push my work to GitHub.
```
```text title="Compare local and remote"
What's the difference between my local copy and what's on GitHub?
```
If a Git concept comes up that you want to understand better — branches, merges, rebases, conflicts — ask the agent to explain it in the context of your project. That's a much faster way to learn than reading Git documentation cold.
In FileMaker, saving and history don't really come up — your changes are just there. In a code project, the equivalent of "saving your work to history" is making a **commit**: a snapshot of the project at a moment in time that you can always come back to.
When you want your work recorded, ask the agent to **commit** your changes. That keyword is what tells the agent you want a Git snapshot, not just an edit. Get in the habit of committing often — every commit is a safe point you can return to.
## Publish your project to GitHub
When you create a new ProofKit project, the local repo on your machine is ready to go — but nothing is on GitHub yet. To get the benefits of remote backup, history you can browse online, and collaboration, you'll want to publish it.
**Create a free GitHub account.**
Go to [github.com](https://github.com) and sign up. The free tier is plenty for personal projects and small teams — there's no need to pay for anything to get started.
**Install the GitHub CLI and sign in.**
The GitHub CLI (`gh`) is what your agent will use to talk to GitHub on your behalf. Install it from [cli.github.com](https://cli.github.com), then sign in by running this once in your terminal:
```bash
gh auth login
```
Follow the prompts to authenticate with your GitHub account. You only need to do this once per machine.
**Ask your agent to publish the project.**
With the CLI installed and authenticated, your agent can create the GitHub repo and push your project up for you.
```text title="Prompt"
Publish this project to a new GitHub repo on my account.
```
The agent will confirm the details before creating anything on your account.
## Next step
From here on, the workflow becomes a habit: edit, commit, push, repeat. Any time you're not sure what to do next, ask your agent — that's the fastest way to learn Git in the context of your own project.
---
# Introduction
URL: https://proofkit.proof.sh/docs/ai
Build modern FileMaker apps with coding agents, WebViewers, and TypeScript tools.
import { Card, Cards } from "fumadocs-ui/components/card";
import { Callout } from "fumadocs-ui/components/callout";
ProofKit helps you build modern web interfaces for FileMaker while keeping your FileMaker file at the center of the app. It connects your AI coding agent to your FileMaker file so it can understand your schema, build a modern Web Viewer app, run checks, preview the result, and deploy the bundle back into FileMaker.
If you're new, follow the guided path below. The package docs are here when you need API details, but the pages in this section walk from first install to a deployed FileMaker app.
## Start Here
Install ProofKit, connect your file, build an app, deploy it, then edit source and redeploy into FileMaker.
See how ProofKit helps agents read, write, verify, and fix FileMaker-backed apps.
Learn when to use ProofKit for conversational FileMaker exploration and when to build a Web Viewer app.
Learn how modern web UI runs inside FileMaker while keeping FileMaker's platform strengths.
Scaffold, preview, and iterate on a FileMaker Web Viewer app with your coding agent.
## What ProofKit gives the agent
* Metadata access to your open FileMaker file.
* Type-safe project scaffolding for a modern web stack.
* A bridge for FileMaker scripts, data, and Web Viewer communication.
* Deployment tooling that bundles the app into the FileMaker file.
* Feedback loops so the agent can inspect errors and iterate.
Start here first, then use the Web Viewer, TypeGen, and FMDAPI sections as reference material when a guide points you there.
## Recommended path
1. Read [Getting Started](/docs/ai/getting-started) for the full journey.
2. Confirm your machine and file meet the [technical requirements](/docs/ai/technical-requirements).
3. Learn why ProofKit starts with [hybrid Web Viewer apps](/docs/webviewer/why-webviewers).
4. Keep the [FAQ](/docs/ai/faq) nearby for scope, runtime, and team workflow questions.
## Tools and Packages
ProofKit projects use the packages below; each part is documented separately if you want to understand or use it directly.
Generate TypeScript types and validation schemas from FileMaker layouts.
Use the Web Viewer bridge to communicate between FileMaker and your app.
Work with the FileMaker Data API from TypeScript.
Use a strongly typed FileMaker OData client.
Add self-hosted auth backed by your FileMaker database.
---
# Install and Connect
URL: https://proofkit.proof.sh/docs/ai/install-and-connect
Install ProofKit and confirm your agent can see your FileMaker file.
import { Step, Steps } from "fumadocs-ui/components/steps";
The first milestone is simple: your coding agent should be able to communicate with an open FileMaker file through ProofKit.
ProofKit installs several pieces that work together: a FileMaker add-on, a FileMaker plug-in, a connector script, an MCP server, and agent integrations.
## Install ProofKit
**Download and run the installer.**
Get the installer for your platform:
The installer sets up the local ProofKit tools and registers integrations for supported coding agents.
When it finishes, **fully restart both FileMaker Pro and your coding agent** (Claude Desktop, Cursor, etc.) so they pick up the new plug-in and integrations. Skipping this restart is the most common cause of connection failures.
**Open your FileMaker file in FileMaker Pro.**
ProofKit works against an open local or hosted file. FileMaker Server is not required for the development loop.
**Add the ProofKit add-on to the file.**
The add-on installs the scripts, layout, table, and custom function ProofKit uses to communicate with your file and store deployed app bundles.
**Enable the ProofKit plug-in.**
In FileMaker Pro, open the Plug-Ins settings and confirm the ProofKit plug-in is enabled. The **Connect to MCP** script needs this plug-in to communicate with the local MCP server.
**Run the Connect to MCP script.**
In FileMaker Pro, run the **Connect to MCP** script installed by the add-on. It opens the connector Web Viewer that registers your file with the local MCP server. Leave the connector Web Viewer open in Browse mode while you work.
## Verify the connection
Open your agent and ask it to confirm that ProofKit can see your FileMaker file. A good first prompt:
```text title="Prompt"
Use ProofKit to verify my FileMaker connection, list the connected file, and summarize the available layouts.
If nothing is connected, tell me what to fix before I continue.
```
*Claude Code following the prompt and confirming the connected file and layouts.*
If the agent cannot see your file, re-run **Connect to MCP** in FileMaker Pro and confirm the connector Web Viewer remains open in Browse mode. For the full checklist, see [fmBridge troubleshooting](/docs/webviewer/fm-bridge#troubleshooting).
## Next step
Once the connection works, continue to [Explore Your File](/docs/ai/explore-your-file).
---
# Manual MCP Setup
URL: https://proofkit.proof.sh/docs/ai/manual-mcp-setup
Register the ProofKit MCP server with a coding agent by hand when the installer cannot do it for you.
import { Callout } from "fumadocs-ui/components/callout";
The ProofKit installer automatically registers the `proofkit-mcp` server with the coding agents it knows about — Claude Desktop, Claude Code, Cursor, Codex, VS Code, Windsurf, and Gemini CLI. This page is for the cases where that automatic step is not enough:
* The installer reported a failure or skipped your agent.
* You want to register ProofKit with an MCP-compatible agent the installer does not detect.
* You want to edit the server arguments by hand, for example to add or remove the [telemetry flag](/docs/ai/telemetry).
You still need to run the ProofKit installer at least once so the `proofkit-mcp` binary, the FileMaker plug-in, and the add-on are present on your machine. This page only covers the agent config file step.
## Where the `proofkit-mcp` binary lives
You will point your agent at the absolute path to the `proofkit-mcp` binary that the installer placed on your machine.
| Platform | Path |
| ------------ | -------------------------------------------------- |
| macOS, Linux | `~/.local/bin/proofkit-mcp` |
| Windows | `%LOCALAPPDATA%\ProofKit\MCP\bin\proofkit-mcp.exe` |
Replace `~` and `%LOCALAPPDATA%` with the fully resolved path in the JSON or TOML snippets below. Most agents require an absolute path.
## The server entry
Every supported agent uses the same logical server entry. The only differences are the file format (JSON or TOML), the wrapper key, and where the file lives.
```json title="The shared server entry"
{
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
```
* `command` — absolute path to `proofkit-mcp`.
* `args` — leave empty for default behavior. Add `"--disable-telemetry"` here to opt out of [anonymous usage statistics](/docs/ai/telemetry).
* `env` — leave empty unless you need an advanced override such as `FM_HTTP_BASE_URL`, `FM_MCP_EXPERIMENTAL_FEATURES`, or `FM_MCP_EXPOSE_LOW_LEVEL_DATA_API_TOOLS`.
## Per-agent config
Open the config file for your agent, add the server entry under the correct wrapper key, save, and restart the agent.
**Config file**
* macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
* Windows: `%APPDATA%\Claude\claude_desktop_config.json`
**Wrapper key:** `mcpServers`
```json
{
"mcpServers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
}
}
```
Quit and reopen Claude Desktop after saving.
**Config file:** `~/.claude.json`
**Wrapper key:** `mcpServers`
```json
{
"mcpServers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
}
}
```
Restart Claude Code after saving.
**Config file:** `~/.cursor/mcp.json`
**Wrapper key:** `mcpServers`
```json
{
"mcpServers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
}
}
```
Restart Cursor after saving.
**Config file:** `~/.codex/config.toml`
Codex uses TOML, not JSON. **Wrapper key:** `mcp_servers`.
```toml
[mcp_servers.proofkit-mcp]
command = "/Users/you/.local/bin/proofkit-mcp"
args = []
[mcp_servers.proofkit-mcp.env]
```
Restart Codex after saving.
**Config file**
* macOS: `~/Library/Application Support/Code/User/mcp.json`
* Windows: `%APPDATA%\Code\User\mcp.json`
**Wrapper key:** `servers` (not `mcpServers`).
```json
{
"servers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
}
}
```
Reload the VS Code window after saving.
**Config file:** `~/.codeium/windsurf/mcp_config.json`
**Wrapper key:** `mcpServers`
```json
{
"mcpServers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
}
}
```
Restart Windsurf after saving.
**Config file:** `~/.gemini/settings.json`
**Wrapper key:** `mcpServers`
```json
{
"mcpServers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": [],
"env": {}
}
}
}
```
Restart the Gemini CLI session after saving.
## Other MCP-compatible agents
Any agent that speaks MCP over stdio can run ProofKit. The agent only needs to launch the `proofkit-mcp` binary as a child process and talk to it with the MCP protocol. Follow your agent's documentation for how to add an MCP server and use the **shared server entry** above as the values.
If your agent supports the standard `mcpServers` wrapper key (Claude Desktop, Cursor, Windsurf, Gemini CLI all use it), the Claude Desktop snippet is a safe starting point.
## Verify the connection
After restarting your agent, ask it to use ProofKit. A good first prompt:
```text title="Prompt"
Use ProofKit to verify my FileMaker connection, list the connected file, and summarize the available layouts.
If nothing is connected, tell me what to fix before I continue.
```
If the agent reports it cannot find the server, double-check the absolute path to `proofkit-mcp` and that the file is executable. On macOS and Linux, you can run the binary directly from a terminal to confirm it launches:
```sh
~/.local/bin/proofkit-mcp --help
```
## Related guides
* [Install and Connect](/docs/ai/install-and-connect)
* [Telemetry](/docs/ai/telemetry)
* [Troubleshooting](/docs/ai/troubleshooting)
---
# Technical Requirements
URL: https://proofkit.proof.sh/docs/ai/technical-requirements
What you need to build and run ProofKit v2 apps.
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
ProofKit v2 is designed for FileMaker developers using agentic coding tools to build Web Viewer apps.
## FileMaker version
ProofKit v2 requires **FileMaker 22 or greater**.
Future ProofKit releases will track the next major FileMaker release cycle so ProofKit can take advantage of new platform capabilities as they become available.
## Development machine
To build ProofKit apps, you need:
* **FileMaker Pro** with the target file open.
* **Node.js LTS** installed locally. Choose a release that is explicitly marked LTS. Today that means **Node.js 22**, **Node.js 24**, or **Node.js 26**.
* **pnpm** version 11 is recommended. It is currently the most secure package manager option.
* **An MCP-compatible coding agent**, such as Cursor, Claude Code, Codex, OpenCode, or another agent that supports MCP servers.
FileMaker Server is not required for the development loop. ProofKit can work against a local or hosted file as long as it is open in FileMaker Pro.
## Node.js
ProofKit projects use Node.js for the local development tools that scaffold, build, preview, and deploy Web Viewer apps. Pick an LTS release rather than the newest "Current" release. Node's odd-numbered releases are short-lived, and not every even-numbered release is the right choice forever; use a version that [nodejs.org marks as LTS](https://nodejs.org/en/download/).
If you are not already managing Node versions, the native installer from nodejs.org is the simplest option.
Download the LTS installer for macOS or Windows from [nodejs.org/download](https://nodejs.org/en/download/). Choose Node.js 26 LTS when available, or Node.js 24 LTS or Node.js 22 LTS if your environment standardizes on one of those versions.
After installing, verify the version:
```sh
node -v
npm -v
```
On macOS, Homebrew is convenient if you already use it. Install a versioned LTS formula instead of the unversioned `node` formula, which may track a newer non-LTS release.
```sh
brew install node@24
node -v
```
If `node` is not found after installation, run `brew info node@24` and follow Homebrew's PATH instructions.
Use a version manager if you switch between projects with different Node requirements.
```sh
nvm install --lts
nvm use --lts
```
`fnm` and Volta are good alternatives. The important part is pinning your project to an LTS line such as Node.js 26, Node.js 24, or Node.js 22.
## pnpm
ProofKit projects use `pnpm` for workspace installs, scripts, and monorepo tooling. We recommend **pnpm 11** because it is the fastest and most secure package manager available for the Node.js ecosystem.
* Learn more: [pnpm.io](https://pnpm.io/)
* Installation guide: [pnpm installation docs](https://pnpm.io/installation)
After installing, verify your version:
```sh
pnpm -v
```
## Developer background
You do not need to be a web developer to get started. Familiarity with React, TypeScript, a terminal, and Git will help, but the workflow is designed so the agent does much of the setup and implementation work.
## Runtime environments
After a Web Viewer app is bundled and deployed into FileMaker, it can run in:
* FileMaker Pro on macOS and Windows.
* FileMaker Go on iOS and iPadOS.
* FileMaker WebDirect.
See [Platform Notes](/docs/webviewer/platform-notes) for runtime caveats.
## Out of scope for v2
ProofKit v2 enables agentic coding of Web Viewer apps. It does not currently enable agentic editing of:
* Scripts.
* Tables and fields.
* Layouts.
* Value lists.
* Other FileMaker schema or design elements.
## Summary
| Requirement | Value |
| ------------------------ | ------------------------------------------- |
| FileMaker version | FileMaker 22 or greater |
| Agent environment | Any MCP-compatible agent |
| Development requirements | FileMaker Pro and Node.js 22, 24, or 26 LTS |
| FileMaker Server | Not required for local development |
| Runtime platforms | FileMaker Pro, FileMaker Go, WebDirect |
| Agent writing scope | Web Viewer app code |
---
# Telemetry
URL: https://proofkit.proof.sh/docs/ai/telemetry
What anonymous usage statistics ProofKit collects, and how to opt in or out.
import { Callout } from "fumadocs-ui/components/callout";
import { Step, Steps } from "fumadocs-ui/components/steps";
ProofKit can send a small amount of anonymous usage data to help us understand which tools are being used and where the MCP server fails. We collect **usage statistics only**. We do not collect anything that identifies you, your FileMaker file, your data, or your code.
Telemetry is keyed to a random ID generated on your machine the first time ProofKit runs. It is not tied to your name, email, license, IP address, FileMaker account, file name, or any other personal or business identifier.
## What we collect
When telemetry is enabled, ProofKit sends:
* **MCP session lifecycle events** — when an MCP session starts and ends, the transport (`stdio`), session duration, the agent client name and version (for example, `Claude Desktop 1.2.3`), and how many tool calls happened in the session.
* **Tool call metadata** — the tool name (for example, `connectedFiles`, `execute_filemaker_sql`), how long it took, and whether it succeeded or failed.
* **Installer completion** — installer UI used (macOS pkg, Windows NSIS, or CLI), platform, duration, exit code, whether telemetry was enabled, and the installer version.
* **A random anonymous ID** — generated on first run and stored locally so repeat sessions from the same machine can be counted as one user.
* **ProofKit server version**.
## What we do NOT collect
ProofKit telemetry **never** includes:
* FileMaker data of any kind — no record values, no field contents, no found sets.
* Tool inputs or arguments passed to any MCP tool.
* Tool outputs or return values.
* SQL queries, layout names, table names, field names, or script names.
* Error messages or stack traces.
* File names, file paths, or anything about your project structure.
* Your name, email, IP address, FileMaker account, or any other identifier.
When a tool call fails, we record only that the tool with that name failed and how long it took. The error itself stays on your machine.
## How to opt in or opt out
The simplest way to change your telemetry choice is to **run the ProofKit installer again** and check or uncheck the **Allow anonymous usage statistics** option.
**Download the latest installer.**
**Run the installer.**
On the installer screen, look for **Allow anonymous usage statistics**.
* **Check the box** to opt in.
* **Uncheck the box** to opt out.
The default is checked (opted in).
**Finish the install.**
Your choice is applied immediately. No restart of FileMaker or your agent is required for the telemetry change itself, although other parts of the installer may ask you to relaunch FileMaker.
You can re-run the ProofKit installer any time to change your telemetry choice. It does not remove your projects or deployed apps. See [Updating ProofKit](/docs/ai/updating-proofkit) for the full update flow if you also want to update the FileMaker add-on.
## Manually disable telemetry
If you cannot re-run the installer, or you registered ProofKit with your agent by hand, you can opt out by adding `--disable-telemetry` to the `args` array of the `proofkit-mcp` entry in your agent's MCP config.
```json title="Example: Claude Desktop, Cursor, Windsurf, Gemini CLI, Claude Code"
{
"mcpServers": {
"proofkit-mcp": {
"command": "/Users/you/.local/bin/proofkit-mcp",
"args": ["--disable-telemetry"],
"env": {}
}
}
}
```
```toml title="Example: Codex"
[mcp_servers.proofkit-mcp]
command = "/Users/you/.local/bin/proofkit-mcp"
args = ["--disable-telemetry"]
```
Save the config and restart your agent. To re-enable telemetry, remove `"--disable-telemetry"` from `args`.
The exact config file path and wrapper key vary by agent. See [Manual MCP Setup](/docs/ai/manual-mcp-setup) for the full list of supported agents, their config file locations, and what to do if your agent is not in that list.
## Why we collect it
Anonymous usage statistics help us:
* See which MCP tools agents actually use, so we can invest in the right ones.
* Spot tools that fail often, so we can fix bugs we would not otherwise hear about.
* Understand which agent clients (Claude Desktop, Cursor, Codex, and others) ProofKit is used with.
* Measure whether installer changes succeed or fail across platforms.
## Related guides
* [Install and Connect](/docs/ai/install-and-connect)
* [Updating ProofKit](/docs/ai/updating-proofkit)
---
# Troubleshooting
URL: https://proofkit.proof.sh/docs/ai/troubleshooting
Fix common ProofKit AI setup, preview, and deployment problems.
## I see an error about the ProofKit plug-in
There are three common plug-in states:
### The plug-in is disabled
If ProofKit is installed but disabled in FileMaker Pro:
1. Open FileMaker Pro.
2. Go to **Settings**.
3. Open the **Plug-Ins** tab.
4. Find the ProofKit plug-in.
5. Enable it.
6. Re-run the **Connect to MCP** script.
### The plug-in failed to load
If FileMaker says the ProofKit plug-in failed to load, report a bug in the [ProofKit community](https://community.proof.sh/c/proofkit).
### The plug-in is missing
If FileMaker says the ProofKit plug-in is missing, download and install ProofKit again.
If you already downloaded and installed it and still see the missing plug-in error, report a bug in the [ProofKit community](https://community.proof.sh/c/proofkit).
## The agent cannot see my FileMaker file
If your agent says no file is connected, or it cannot list layouts or fields:
1. Confirm the file is open in FileMaker Pro.
2. Re-run the **Connect to MCP** script in that file.
3. Keep the connector Web Viewer window open while you work.
4. Confirm that connector window is in **Browse mode**, not Layout mode.
5. Restart the agent app after installing ProofKit if this is your first setup.
This is the most common failure mode. The connector Web Viewer is part of the bridge. If it is closed, hidden in the wrong file window, or switched out of Browse mode, the agent loses access.
If the connection still fails, revisit [Install and Connect](/docs/ai/install-and-connect) and the [`fmBridge` troubleshooting notes](/docs/webviewer/fm-bridge#troubleshooting).
## The local preview opens, but FileMaker-backed features do not work
If the browser preview loads but data calls, script calls, or bridge actions fail:
* Ensure the FileMaker file is still connected through **Connect to MCP**.
* Confirm the connector Web Viewer has not been closed since the dev server started.
* Check that your project is using the standard ProofKit Web Viewer setup with `fmBridge`.
* Re-run your dev server after reconnecting FileMaker if the bridge was broken during startup.
Typical symptoms include script calls hanging, bridge errors, or a preview that renders static UI but never loads live FileMaker data.
For bridge-specific failures, see [`fmBridge`](/docs/webviewer/fm-bridge) and [Web Viewer troubleshooting](/docs/webviewer/troubleshooting).
## The generated app is using the wrong fields
If the agent built against stale schema, or TypeScript errors reference fields that no longer exist:
1. Make sure the agent is using the correct FileMaker file and layout.
2. Re-run type generation after schema or layout changes.
3. Restart the dev server after regenerated types land.
4. Ask the agent to fix type errors before continuing feature work.
ProofKit apps rely on generated metadata. If your FileMaker schema changes but the project still uses old generated files, the agent will confidently write code against the wrong shape.
Use the [TypeGen docs](/docs/typegen) when the issue is clearly about generated types, layouts, or config.
## FileMaker says access privileges are damaged
If FileMaker shows a warning that says **"The access privileges in this file have been damaged or possibly tampered with"**, and the file previously had **PROOFKIT** installed in it, contact us at [support@proof.sh](mailto:support@proof.sh).
This issue only affected files where the ProofKit add-on was installed with **ProofKit 2.04**. We fixed the add-on in later versions, but in affected files the 2.04 add-on could fail to uninstall cleanly.
We have a fix for affected files. Send us a note at [support@proof.sh](mailto:support@proof.sh), mention that the file had **PROOFKIT** installed, and we can help repair it.
---
# Updating ProofKit
URL: https://proofkit.proof.sh/docs/ai/updating-proofkit
Re-run the installer, then reconnect FileMaker, reinstall the add-on, and redeploy your apps.
import { Callout } from "fumadocs-ui/components/callout";
import { Step, Steps } from "fumadocs-ui/components/steps";
import { CliCommand } from "@/components/CliCommand";
To update ProofKit, simply run the latest installer on your system. Sometimes this is all you need to do. If, however, you also want to update the add-on that was installed in your FileMaker file, you'll need to follow a few extra steps.
## Download the latest installer
## Updating the Add-on in FileMaker
**Restart FileMaker Pro** if it was running during the installation process.
Close FileMaker Pro fully, then open it again before you continue.
**Uninstall the ProofKit add-on from your FileMaker file.**
This may remove all previously deployed apps from your file. You will need to make sure that you still have the source code (a folder on your computer) so you can deploy them again in a later step.
Go into layout mode and the add-ons tab on the left sidebar. Right-click the ProofKit add-on and choose to uninstall.
**Install the Add-on again.**
After installation, you can verify that the new add-on was installed by checking the version number in any of the ProofKit scripts.
**Redeploy your apps into the FileMaker file.**
Remember to run the "Connect to MCP" script in order to open the connection to your FileMaker file before deploying.
Any Web Viewer apps you want available in that file should be deployed again from their local source folders. You can either ask your coding agent to do this for you or run this command within that folder:
## Related guides
* [Install and Connect](/docs/ai/install-and-connect)
* [Deploy to FileMaker](/docs/ai/deploy-to-filemaker)
---
# Introduction
URL: https://proofkit.proof.sh/docs/better-auth
@proofkit/better-auth
import { Cards, Card } from "fumadocs-ui/components/card";
import { SquareArrowOutUpRight } from "lucide-react";
# What is Better-Auth?
From the [Better-Auth](https://better-auth.com) website:
> The most comprehensive authentication framework for TypeScript.
Better-Auth has a robust plugin ecosystem that gives you the ability to pick and choose which authentication methods you want to use, all within a system that let's you securely host your own authentication.
The `@proofkit/better-auth` package allows you to use FileMaker as the backend database for the better-auth library, including automated schema migrations to easily add any neccesary tables and fields to your FileMaker file.
How to quickly get started with Better-Auth and FileMaker.
Better-Auth Docs{" "}
>
}
href="https://better-auth.com/docs"
>
Learn more about the Better-Auth framework and how to use it in your app.
---
# Installation & Usage
URL: https://proofkit.proof.sh/docs/better-auth/installation
import { CliCommand } from "@/components/CliCommand";
import { PackageInstall } from "@/components/PackageInstall";
import { Callout } from "fumadocs-ui/components/callout";
# Prerequisites
* Ensure OData is enabled on your FileMaker server.
* Ensure your credentials have the `fmodata` privilege enabled.
* If you are using OttoFMS 4.11+ and you want to use a Data API key instead of plain credentials, ensure OData is enabled for that key.
# Step 1: Manual Setup
Follow the [Better-Auth installation guide](https://better-auth.com/docs/installation) to get started in your app, but come back here for special instructions for anything related to your Database Setup or schema migrations.
### Database Setup
Ensure you have the @proofkit/better-auth and @proofkit/fmodata packages installed in your app.
Configure your database connection in your `auth.ts` file. Be sure to set these value secrets in your environment variables. The credentials you use here need `fmodata` permissions enabled, and read/write access to the better-auth tables.
```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { FMServerConnection } from "@proofkit/fmodata";
import { FileMakerAdapter } from "@proofkit/better-auth";
const connection = new FMServerConnection({
serverUrl: process.env.FM_SERVER_URL!,
auth: {
// option 1: username/password credentials
username: process.env.FM_USERNAME!,
password: process.env.FM_PASSWORD!,
// option 2: Data API key (OttoFMS 4.11+, OData enabled for the key)
// apiKey: process.env.OTTO_API_KEY!,
},
});
const db = connection.database(process.env.FM_DATABASE!);
export const auth = betterAuth({
database: FileMakerAdapter({ database: db }),
// ...rest of your config
});
```
# Step 2: Create/Update Database Tables
Run the following command to create the necessary tables and fields in your FileMaker file. It will show you a confirmation before any changes are applied, so you can review them.
\[Full Access] credentials are required for the schema changes to be applied automatically, but you may want to use a more restricted account for the rest of better-auth usage. If your credentials that you entered earlier in the `auth.ts` file do not have the \[Full Access] permissions, you can override them in the CLI.
These changes affect database schema only. No layouts or relationships are created or modified during this process.
The tables/fields that are created will be dependent on how your `auth.ts` file is setup. If you want to use any of your existing tables, just set [custom table names](https://www.better-auth.com/docs/concepts/database#custom-table-names) in the `auth.ts` file before running the migration command.
You may see fields added to your tables that you don't plan on using, but it's best to keep them in your database anyway to avoid potential errors.
If you make any schema-related changes to the better-auth config, such as adding plugins, you will need to run the migration command again to apply the changes to your FileMaker file.
---
# Troubleshooting
URL: https://proofkit.proof.sh/docs/better-auth/troubleshooting
import { CliCommand } from "@/components/CliCommand";
## Error when generating schema
```bash
ERROR [Better Auth]: filemaker is not supported. If it is a custom adapter, please request the maintainer to implement createSchema
```
This means you used the better-auth CLI directly instead of the @proofkit/better-auth version. Run this instead:
---
# Adapters
URL: https://proofkit.proof.sh/docs/fmdapi/adapters
Adapters are a new level of abstraction that allows you to reuse the same client API with any number of proxies, including via a FileMaker Webviewer using the [`@proofkit/webviewer`](/docs/webviewer/package) package and the Execute Data API script step. The adapter is responsible for handling the specifics of the connection to the FileMaker Data API, while the shared client exposes helper functions, schema validators, and other utilities to the developer.
Choose the adapter that is right for your project, or view below for how to build your own custom adatper.
## OttoFMS (recommended)
To connect via the [OttoFMS](https://docs.ottofms.com/) [Data API Proxy](https://docs.ottofms.com/ottofms-features/api-proxy#data-api-proxy), use the `OttoAdapter` with a Data API key:
```ts
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
```
The OttoAdapter is compatible with API keys for both the Otto v3 and OttoFMS Data API Proxy. OttoFMS is available under a free license and is our recommended method for interacting with the Data API.
#### Options
| Option | Type | Description |
| ------------- | -------- | ---------------------------------------------------------------------------------------- |
| `auth.apiKey` | `string` | The Data API key from either Otto v3 (starts with `KEY_`) or OttoFMS (starts with `dk_`) |
| `auth.port` | `string` | *(optional)* Only used for Otto v3. Defaults to 3030 |
| `db` | `string` | FileMaker database name |
| `server` | `string` | FileMaker server URL (must start with include `https://`) |
## FetchAdapter
To connect directly to the FileMaker Data API, use the `FetchAdapter` with a username and password:
```ts
import { DataApi, FetchAdapter } from "@proofkit/fmdapi";
```
#### Options
| Option | Type | Description |
| ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------ |
| `auth` | `object` | Authentication object. Must contain `username` and `password` |
| `db` | `string` | FileMaker database name |
| `server` | `string` | FileMaker server URL (must include `https://`) |
| `tokenStore` | `TokenStore` | *(optional)* If provided, will use the custom set of functions to store and retrieve the short-lived access token. |
## WebViewerAdapter
For rich webviewer experiences, use the `WebViewerAdapter` with the [`@proofkit/webviewer`](/docs/webviewer/package) 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
```
Then import the adapter like so:
```ts
import { DataApi } from "@proofkit/fmdapi";
import { WebViewerAdapter } from "@proofkit/webviewer";
```
## Custom Adapters
> **This is an advanced topic.** If you are just an application developer trying to connect to a FileMaker database, all you need to know is how you want to connect to the FileMaker server, then import the appropriate adapter. Type hint for the selected adapter will guide you through the rest.
If you want to write you own adapter for your own proxy, or to override the root-level `request` method, you can write a custom adapter.
All adapters must implement the `Adapter` interface. If you want to build a proxy similar to the `OttoAdapter`, you can extend the `BaseFetchAdapter` class and will likely only need to implement the `getToken` and `request` methods. View the source for the FetchAdapter for an example of this.
---
# Usage Examples
URL: https://proofkit.proof.sh/docs/fmdapi/examples
import { Callout } from "fumadocs-ui/components/callout";
import { Card, Cards } from "fumadocs-ui/components/card";
## Prerequisites
Before you can use any of these methods, you need to set up a FileMaker Data API client. If you haven't done this yet, check out our [Quick Start Guide](/docs/fmdapi/quick-start) to create your client in a separate file.
For these examples, we'll assume you've already setup a client in another file and are importing it for use.
```ts
// This is just an example - follow the Quick Start guide for your actual setup
import { CustomersLayout } from "./schema/client";
```
***
## Retrieving Data from FileMaker
### Finding Records
You can use the `find` method to perform FileMaker-style find requests on a layout. This method is limited to 100 records by default, but you can use the `findAll` helper method to automatically paginate the results and return all records that match the query.
```ts twoslash title="searchCustomers.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
// Simple search - find customers in a specific city (max 100 records)
export async function findCustomersByCity(city: string) {
const response = await CustomersLayout.find({
query: { city: city }
});
console.log(`Found ${response.data.length} customers in ${city}`);
return response.data;
}
// Get ALL matching records at once (handles pagination automatically)
export async function getAllActiveCustomers() {
const records = await CustomersLayout.findAll({
query: { status: "==Active" } // all standard FileMaker operators are supported
});
return records; // All active customers, no pagination needed
}
```
Use an array of find requests to get the OR behavior, equivalent to having multiple find requests in FileMaker.
```ts twoslash title="multipleFindRequests.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
export async function getCustomersByCityOrStatus(city: string, status: string) {
const records = await CustomersLayout.findAll({
query: [{ city }, { status }]
});
return records;
}
```
There are also helper methods for common find scenarios. Any of these methods will return just a single record instead of an array.
* `findOne` will throw an error unless there is exactly one record found
* `findFirst` will return the first record found, but still throw if no records are found
* `maybeFindFirst` will return the first record found or null
### Getting All Records
If you don't need to specify any find requests, you can use the `list` or `listAll` methods. `list` will limit to 100 records per request by default, while `listAll` will automatically handle pagination via the API and return all records for the entire table. Use with caution if the table is large!
```ts twoslash title="getAllCustomers.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
// Get a page of customers (recommended for large datasets)
export async function listCustomers() {
const response = await CustomersLayout.list({
sort: [{ fieldName: "firstName", sortOrder: "ascend" }]
});
return {
customers: response.data,
totalRecords: response.dataInfo.foundCount,
hasMore: response.data.length === 100 // Default page size
};
}
// Get ALL customers at once (use with caution on large datasets)
export async function listAllCustomers() {
const records = await CustomersLayout.listAll();
type CustomerRecord = (typeof records)[number];
console.log(`Retrieved all ${records.length} customers`);
return records.map((customer: CustomerRecord) => ({
id: customer.recordId,
firstName: customer.fieldData.firstName,
lastName: customer.fieldData.lastName,
email: customer.fieldData.email,
city: customer.fieldData.city
}));
}
```
***
## Creating Records
Use `create` to add new records to your FileMaker database.
```ts twoslash title="createCustomer.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
export async function createNewCustomer(customerData: {
firstName: string;
lastName: string;
email: string;
phone?: string;
city?: string;
}) {
const response = await CustomersLayout.create({
fieldData: {
firstName: customerData.firstName,
lastName: customerData.lastName,
email: customerData.email,
phone: customerData.phone || "",
city: customerData.city || "",
status: "Active",
created_date: new Date().toISOString()
}
});
console.log(`Created customer with ID: ${response.recordId}`);
return response.recordId;
}
```
***
## Update / Delete Records
Updating or deleting records requires the internal record id from FileMaker, not the primary key for your table. This value is returned in the `recordId` field of any create, list, or find response.
This record id *can* change during imports or data migrations, so you should never store it, but instead alwyas look it up via a find request when it's needed.
```ts twoslash title="updateCustomer.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
export async function updateCustomerInfo(myPrimaryKey: string, updates: {
firstName?: string;
lastName?: string;
phone?: string;
city?: string;
}) {
const { data: { recordId } } = await CustomersLayout.findOne({ query: { myPrimaryKey: myPrimaryKey } });
// Only update fields that were provided
const fieldData: any = {};
if (updates.firstName) fieldData.firstName = updates.firstName;
if (updates.lastName) fieldData.lastName = updates.lastName;
if (updates.phone) fieldData.phone = updates.phone;
if (updates.city) fieldData.city = updates.city;
const response = await CustomersLayout.update({ fieldData, recordId });
return response.modId;
}
```
```ts twoslash title="deleteCustomer.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
export async function deleteCustomer(myPrimaryKey: string) {
// Optional: Get customer info first for logging
const { data: { recordId } } = await CustomersLayout.findOne({ query: { myPrimaryKey: myPrimaryKey } });
await CustomersLayout.delete({recordId});
}
```
***
## Working with Scripts
FileMaker scripts can be executed during any other method or run directly.
### Running Scripts Directly
Use `executeScript` to run a script directly.
```ts twoslash title="executeScripts.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
export async function sendEmailFromFileMaker() {
const response = await CustomersLayout.executeScript({
script: "Send Email",
scriptParam: JSON.stringify({
to: "customer@example.com",
subject: "Welcome to our service",
body: "Thank you for signing up!"
})
});
console.log("Script result:", response.scriptResult);
return response.scriptResult;
}
```
### Run a script with another method
You can run scripts before or after any data operation. The script will be run in the context of the layout specified in your client and will be on the record or found set as the operation. This is especially useful when creating records, as you can run a script after the record is created, knowing the script will be focused on this newly created record; thus giving you access to the calculated values such as a UUID primary key defined in your field definitions.
```ts twoslash title="scriptsWithOperations.ts"
import { CustomersLayout } from "./CustomersLayout";
// ---cut---
// Run a script after creating a record
export async function createCustomerWithWelcomeEmail(customerData: any) {
const response = await CustomersLayout.create({
fieldData: customerData,
script: "Send Welcome Email", // script name
// script param must always be a string
"script.param": JSON.stringify({
email: customerData.email,
name: `${customerData.firstName} ${customerData.lastName}`
})
});
return {
recordId: response.recordId,
scriptResult: response.scriptResult
};
}
```
For more details about the script execution order, see [this page](https://help.claris.com/en/data-api-guide/content/run-script-with-another-request.html) of the FileMaker Data API guide.
***
See also
Complete list of all available methods
Set up your FileMaker Data API client
---
# Overview
URL: https://proofkit.proof.sh/docs/fmdapi
@proofkit/fmdapi
This package is designed to make working with the FileMaker Data API much easier. Here's just a few key features:
For generated ProofKit Web Viewer apps, start with [Hybrid App Data Access](/docs/webviewer/data-access) to understand when this package is used directly and when FileMaker scripts are the better runtime path.
* Handles token refresh for you automatically
* [OttoFMS](https://ottofms.com/) Data API proxy support
* TypeScript support for easy auto-completion of your field names
* [Automated type generation](/docs/typegen) based on layout metadata
* Protection against field name changes with [Standard Schema](https://standardschema.dev/) runtime validation
* Supports both node and edge runtimes with a customizable token store
* Customizable adapters allow usage even within the [FileMaker Web Viewer](/docs/webviewer/package)
## Edge Runtime Support
This package is compatible with edge runtimes, but there are some additional considerations to avoid overwhelming your FileMaker server with too many connections. If you are developing for the edge, be sure to implement one of the following strategies:
* ✅ Use a custom token store (see above) with a persistent storage method such as Upstash
* ✅ Use a proxy such as the [Otto Data API Proxy](https://www.ottofms.com/docs/otto/working-with-otto/proxy-api-keys/data-api) which handles management of the access tokens itself.
* Providing an API key to the client instead of username/password will automatically use the Otto proxy
---
# Quick Start - Manual
URL: https://proofkit.proof.sh/docs/fmdapi/manual-setup
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
import { Callout } from "fumadocs-ui/components/callout";
> Note: For the best experience, use the [@proofkit/typegen](/docs/typegen) tool to generate layout-specific clients and get autocomplete hints in your IDE with your actual field names. This minimal example just demonstrates the basic setup
Add the following envnironment variables to your project's `.env` file:
```sh
FM_DATABASE=filename.fmp12
FM_SERVER=https://filemaker.example.com
# if you want to use the OttoFMS Data API Proxy
OTTO_API_KEY=dk_123456...789
# otherwise
FM_USERNAME=admin
FM_PASSWORD=password
```
Initialize the client with credentials, depending on your adapter
```ts
// to use the OttoFMS Data API Proxy
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
const client = DataApi({
adapter: new OttoAdapter({
auth: { apiKey: process.env.OTTO_API_KEY },
db: process.env.FM_DATABASE,
server: process.env.FM_SERVER,
}),
layout: "API_Contacts",
});
```
```ts
// to use the raw Data API
import { DataApi, FetchAdapter } from "@proofkit/fmdapi";
const client = DataApi({
adapter: new FetchAdapter({
auth: {
username: process.env.FM_USERNAME,
password: process.env.FM_PASSWORD,
},
db: process.env.FM_DATABASE,
server: process.env.FM_SERVER,
}),
layout: "API_Contacts",
});
```
Then, use the client to query your FileMaker database. [View all available methods here](https://github.com/proofsh/fmdapi/wiki/methods).
Basic Example:
```typescript
const result = await client.list({ layout: "Contacts" });
```
## TypeScript Support
If you define a schema in your client, the types will be inferred automatically. [Learn more](/docs/fmdapi/validation)
The basic client will return the generic FileMaker response object by default. You can also create a type for your expected response and get a fully typed response that includes your own fields.
```typescript
type TContact = {
name: string;
email: string;
phone: string;
};
// if you have portals
type TOrders = {
"Orders::orderId": string;
"Orders::orderDate": string;
"Orders::orderTotal": number;
};
type TPortals = {
orders: TOrders; // key is based on the portal object name
};
const client = DataApi({
layout: "API_Contacts",
// ... your adapter, other config
});
```
💡 TIP: For a more ergonomic TypeScript experience, use the [@proofkit/typegen tool](/docs/typegen) to generate these types based on your FileMaker layout metadata.
---
# Methods
URL: https://proofkit.proof.sh/docs/fmdapi/methods
The following methods are available for all adapters.
* `list` return all records from a given layout
* `find` perform a FileMaker find
* `get` return a single record by recordID
* `create` return a new record
* `update` modify a single record by recordID
* `delete` delete a single record by recordID
# Helper Functions
This package also includes some helper methods to make working with Data API responses a little easier:
* `findOne` return the first record from a find instead of an array. This method will error unless exactly 1 record is found.
* `findFirst` return the first record from a find instead of an array, but will not error if multiple records are found.
* `findAll` return all found records from a find, automatically handling pagination. Use caution with large datasets!
* `listAll` return all records from a given layout, automatically handling pagination. Use caution with large datasets!
# Adapter-Specific Functions
The first-party `FetchAdapter` and `OttoAdatper` both share the following additional methods from the `BaseFetchAdapter`:
* `executeScript` execute a FileMaker script directly
* `layoutMetadata` return metadata for a given layout
* `layouts` return a list of all layouts in the database (top-level layout key ignored)
* `scripts` return a list of all scripts in the database (top-level script key ignored)
* `globals` set global fields for the current session (top-level globals key ignored)
If you have your own proxy, you can write your own Custom Adapter that extends the BaseFetchAdapter to also implement these methods.
## Fetch Adapter
* `disconnect` forcibly logout of your FileMaker session
## Otto Adapter
No additional methods
---
# Quick Start - Typegen
URL: https://proofkit.proof.sh/docs/fmdapi/quick-start
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
import { Steps, Step } from "fumadocs-ui/components/steps";
import { AgentCommand } from "@/components/AgentCommand";
The typegen tool is the best way to interact with this library, as it will automatically generate layout-specific clients and get autocomplete hints in your IDE with your actual field names from your solution
### Install the required packages
npm
pnpm
yarn
bun
```bash
npm install @proofkit/fmdapi zod
```
```bash
pnpm add @proofkit/fmdapi zod
```
```bash
yarn add @proofkit/fmdapi zod
```
```bash
bun add @proofkit/fmdapi zod
```
Zod is used by the typegen tool by default, but it can be excluded if you set `validator` to `false` in the typegen config.
#### AI Agent Integration
If you use an AI coding agent, run this command to map ProofKit's built-in skills to your project. See the [TanStack Intent docs](https://tanstack.com/intent/latest/docs/getting-started/quick-start-consumers) for more details.
### Create a typegen config file in your project
```sh
npx @proofkit/typegen
```
Add the layouts you want to generate clients for to the `layouts` array in the config file.
```jsonc title='proofkit-typegen.config.jsonc'
{
"$schema": "https://proofkit.proof.sh/typegen-config-schema.json",
"config": {
"clientSuffix": "Layout",
"layouts": [
// add your layouts and name schemas here
{ "layoutName": "my_layout", "schemaName": "MySchema" }
// repeat as needed for each layout...
// { layoutName: "my_other_layout", schemaName: "MyOtherSchema" },
],
// change this value to generate the files in a different directory
"path": "schema",
"clearOldFiles": true
},
}
```
### Setup Environment Variables
Add the following envnironment variables to your project's `.env` file:
```bash title=".env"
FM_SERVER=https://filemaker.example.com # must start with https://
FM_DATABASE=filename.fmp12 # must end with .fmp12
# if you want to use the OttoFMS Data API Proxy (recommended)
OTTO_API_KEY=dk_123456...789
# otherwise
FM_USERNAME=admin
FM_PASSWORD=password
```
### Generate the layout-specific clients
Run this command any time you make changes to your config file, any of the referenced FileMaker layouts, or any field names/types that are on the layouts.
```sh
npx @proofkit/typegen
```
Tip: Add a script to your `package.json` to make it easier to run in the future.
```jsonc title="package.json"
{
"scripts": {
// ...
"typegen": "npx @proofkit/typegen"
}
}
```
## Usage
You can now import the layout-specifc client for use in your project.
```ts title="getCustomer.ts"
import { CustomersLayout } from "./schema/client";
export async function getCustomer(id: string) {
// findOne will throw an error unless exactly 1 record is returned
const { data } = await CustomersLayout.findOne({
query: {
id: `==${id}`
}
});
return data.fieldData;
}
```
For all available methods, see
[this page](/docs/fmdapi/methods)
.
## Customization
If you run into any limitations from the generated code, there are many ways to customize it to your needs.
How to configure the typegen tool for your use case.
Customize the shape of the data returned from your database.
---
# Token Store
URL: https://proofkit.proof.sh/docs/fmdapi/token-store
This page applies only to the FetchAdapter when your connecting via username/password to your FileMaker server. The Otto Data API Proxy manages the token for you.
If you are using username/password authentication, the fmdapi client will manage your access token for you. By default, the token is kept in memory only, but you can provide other getter and setter methods to store the token in a database or other location. Included in this package are helper functions for file storage if you have access to the filesystem, or Upstash if running in a serverless environment.
```ts
import { DataApi, FetchAdapter } from "@proofkit/fmdapi";
// using file storage, if you have persistent access to the filesystem on your server
import { fileTokenStore } from "@proofkit/fmdapi/tokenStore/file";
const client = DataApi({
adapter: new FetchAdapter({
// ...
tokenStore: fileTokenStore(),
}),
});
// or with Upstash, requires `@upstash/redis` as peer dependency
import { upstashTokenStore } from "@proofkit/fmdapi/tokenStore/upstash";
const client = DataApi({
adapter: new FetchAdapter({
// ...
tokenStore: upstashTokenStore({
token: process.env.UPSTASH_TOKEN,
url: process.env.UPSTASH_URL,
}),
}),
});
```
---
# Validation & Transformation
URL: https://proofkit.proof.sh/docs/fmdapi/validation
Protect your app from field name changes by validating the shape of the data returned from the FileMaker Data API.
import { File, Folder, Files } from "fumadocs-ui/components/files";
import { IconFileTypeTs } from "@tabler/icons-react";
## Why validate?
One of the best features of the FileMaker platform is how you can easily add, remove, and rename fields in your database. However, when you make integrations that are beyond the scope of your database, you can run into serious issues if the field names change out from under you.
This library supports validation using [Standard Schema](https://standardschema.dev/) out of the box, and will throw an error *early* to prevent unexpected behavior in your app. We suggest using the [Zod](https://zod.dev) library by default, but any other validation library that supports Standard Schema will also work.
## How does it work?
When creating your layout client, add a `schema` property and define a Standard-Schema compliant schema definition for your `fieldData` and `portalData`. Your schema must be an object, and should represent a single record or portal row.
Most validation libraries also support some kind of transformation, and you can use these to adjust how your FileMaker data is returned to your app.
Try this interactive example that uses the [Zod](https://zod.dev/) library to validate and transform the data:
## Handling errors
If you define a schema, each method that you call may cause the API request to throw a validation error, thus no data returned to your application (even if the FileMaker Server gave an OK status code). You should always try/catch these methods and update your schemas when your field names change. The easiest way to keep your schemas up to date is with the [@proofkit/typegen](/docs/typegen) package.
## Typegen Integration
If you use [@proofkit/typegen](/docs/typegen) to generate the layout-specific clients, it will automatically generate zod validators also.
These generated files should **never** be edited, since your changes would just be overwritten the next time you run an update to your schema. However, you can and should add overrides to the files at the root of your schemas, as only the files in the `client` and `generated` folders will be overwritten.
---
# Upgrading to v5
URL: https://proofkit.proof.sh/docs/fmdapi/version-5
import { Callout } from "fumadocs-ui/components/callout";
In v5, we've split the Data API and typegen functionality into two separate packages: `@proofkit/fmdapi` and `@proofkit/typegen`, as well as introduces a few breaking changes.
## Codemod
To make the transition as smooth as possible, we've made an upgrade script to the `@proofgeist/fmdapi`
package. Simply run this command in the root of your project:
```bash
npx @proofgeist/fmdapi@latest upgrade
```
This script will:
* Install the new packages (@proofkit/fmdapi and @proofkit/typegen)
* Remove the old package (@proofgeist/fmdapi)
* Migrate your existing config file to the new format
* Attempt to re-run the typegen command to generate new clients
After running the command, you'll likely want to run `tsc` to check for any issues. You may need to simply do a full project find/replace for `@proofgeist/fmdapi` and replace with `@proofkit/fmdapi`.
We also suggest creating/updating the "typegen" script in your `package.json` file to use the new `@proofkit/typegen` package.
```json
"typegen": "npx @proofkit/typegen"
```
## What's new?
### Validation / Transformations
Zod is no longer required as a peer dependency, and you can now use any library that supports [Standard Schema](https://standardschema.dev/) as your runtime validator. Zod will still be used by the typegen package by default, but only if you want to use it in your runtime application.
Each method called will now also return the result of your validator, so you can define custom transformations if supported by your validation library.
Here's an example of how you might use zod to force a number field to a boolean, or a string to a JavaScript Date:
```ts title="schema/Customers.ts"
import { z } from "zod/v4";
import { ZCustomers as ZCustomers_generated } from "./generated/Customers";
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(customParseFormat);
export const ZCustomers = ZCustomers_generated.omit({ active: true }).extend({
active: z.coerce.boolean(),
createdAt: z
.string()
.transform((v) => dayjs(v, ["MM/DD/YYYY"]))
.toDate(),
});
```
```ts title="index.ts"
import { CustomersLayout } from "./schema/client";
const { data } = await CustomersLayout.list();
data[0].fieldData.active; // --> boolean
```
### Typegen
The typegen features have been isolated from the Data API package and are now available as a separate package: `@proofkit/typegen`. The typegen can be run exclusively with npx so you don't even need to install it as a dev dependency. This should make the package size even smaller and more focused on its core functionality.
Additionally, the generated code now allows you to specify overrides for each schema. Only the files in the `client` and `generated` folders will be overwritten, allowing you to write custom schemas or transformers in the files in the root of the generated folder.
## Breaking Changes
### Layout-specific clients only
Due to the change in how the runtime validators are now processed, it's now **required** to pass a layout name when initializing a client, and you can no longer override the layout per method. If you were exclusvily using generated clients from the typegen features, this should not affect you.
### Token Store removed from typegen
This was deprecated in v4 and is now removed **from typegen only**. You can still use a custom token store, but you will need to modify the typegen options and set `generateClient` to `false` so that you can use the generated types and/or validators, but create your own clients with your own token store for the Fetch adapter.
### Typegen config updates
For full details about the new typegen package, please see the [Typegen docs](/docs/typegen).
* Within the root config:
* `schemas` has been renamed to `layouts`
* `useZod` has been removed
* New option: `validator` can be set to `zod` or `false`
* Within the `layouts` config:
* `layout` has been renamed to `layoutName`
---
# Batch Operations
URL: https://proofkit.proof.sh/docs/fmodata/batch
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
Batch operations allow you to execute multiple queries and operations together in a single request. All operations in a batch are executed atomically - they all succeed or all fail together. This is both more efficient (fewer network round-trips) and ensures data consistency across related operations.
## Batch Result Structure
Batch operations return a `BatchResult` object that contains individual results for each operation. Each result has its own `data`, `error`, and `status` properties, allowing you to handle success and failure on a per-operation basis:
```typescript
type BatchItemResult = {
data: T | undefined;
error: FMODataErrorType | undefined;
status: number; // HTTP status code (0 for truncated operations)
};
type BatchResult = {
results: { [K in keyof T]: BatchItemResult };
successCount: number;
errorCount: number;
truncated: boolean; // true if FileMaker stopped processing due to an error
firstErrorIndex: number | null; // Index of the first operation that failed
};
```
## Basic Batch with Multiple Queries
Execute multiple read operations in a single batch:
```typescript
// Create query builders
const contactsQuery = db.from(contacts).list().top(5);
const usersQuery = db.from(users).list().top(5);
// Execute both queries in a single batch
const result = await db.batch([contactsQuery, usersQuery]).execute();
// Access individual results
const [r1, r2] = result.results;
if (r1.error) {
console.error("Contacts query failed:", r1.error);
} else {
console.log("Contacts:", r1.data);
}
if (r2.error) {
console.error("Users query failed:", r2.error);
} else {
console.log("Users:", r2.data);
}
// Check summary statistics
console.log(`Success: ${result.successCount}, Errors: ${result.errorCount}`);
```
## Mixed Operations (Reads and Writes)
Combine queries, inserts, updates, and deletes in a single batch:
```typescript
// Mix different operation types
const listQuery = db.from(contacts).list().top(10);
const insertOp = db.from(contacts).insert({
name: "John Doe",
email: "john@example.com",
});
const updateOp = db.from(users).update({ active: true }).byId("user-123");
// All operations execute atomically
const result = await db.batch([listQuery, insertOp, updateOp]).execute();
// Access individual results
const [r1, r2, r3] = result.results;
if (r1.error) {
console.error("List query failed:", r1.error);
} else {
console.log("Fetched contacts:", r1.data);
}
if (r2.error) {
console.error("Insert failed:", r2.error);
} else {
console.log("Inserted contact:", r2.data);
}
if (r3.error) {
console.error("Update failed:", r3.error);
} else {
console.log("Updated user:", r3.data);
}
```
## Handling Errors in Batches
When FileMaker encounters an error in a batch operation, it **stops processing** subsequent operations. Operations that were never executed due to an earlier error will have a `BatchTruncatedError`:
```typescript
import { BatchTruncatedError, isBatchTruncatedError } from "@proofkit/fmodata";
const result = await db.batch([query1, query2, query3]).execute();
const [r1, r2, r3] = result.results;
// First operation succeeded
if (r1.error) {
console.error("First query failed:", r1.error);
} else {
console.log("First query succeeded:", r1.data);
}
// Second operation failed
if (r2.error) {
console.error("Second query failed:", r2.error);
console.log("HTTP Status:", r2.status); // e.g., 404
}
// Third operation was never executed (truncated)
if (r3.error && isBatchTruncatedError(r3.error)) {
console.log("Third operation was not executed");
console.log(`Failed at operation ${r3.error.failedAtIndex}`);
console.log(`This operation index: ${r3.error.operationIndex}`);
console.log("Status:", r3.status); // 0 (never executed)
}
// Check if batch was truncated
if (result.truncated) {
console.log(`Batch stopped early at index ${result.firstErrorIndex}`);
}
```
## Transactional Behavior
Batch operations are transactional for write operations (inserts, updates, deletes). If any operation in the batch fails, all write operations are rolled back:
```typescript
const result = await db
.batch([
db.from(users).insert({ username: "alice", email: "alice@example.com" }),
db.from(users).insert({ username: "bob", email: "bob@example.com" }),
db.from(users).insert({ username: "charlie", email: "invalid" }), // This fails
])
.execute();
// Check individual results
const [r1, r2, r3] = result.results;
if (r1.error || r2.error || r3.error) {
// All three inserts are rolled back - no users were created
console.error("Batch had errors:");
if (r1.error) console.error("Operation 1:", r1.error);
if (r2.error) console.error("Operation 2:", r2.error);
if (r3.error) console.error("Operation 3:", r3.error);
}
```
## Important Notes
* **FileMaker stops on first error**: When an error occurs, FileMaker stops processing subsequent operations in the batch. Truncated operations will have `BatchTruncatedError` with `status: 0`.
* **Insert operations in batches**: FileMaker ignores `Prefer: return=representation` in batch requests. Insert operations return `{}` or `{ ROWID?: number }` instead of the full created record.
* **All results are always defined**: Every operation in the batch will have a corresponding result in `result.results`, even if it was never executed (truncated operations).
* **Summary statistics**: Use `result.successCount`, `result.errorCount`, `result.truncated`, and `result.firstErrorIndex` for quick batch status checks.
Batch operations automatically group write operations (POST, PATCH, DELETE) into changesets for transactional behavior, while read operations (GET) are executed individually within the batch.
---
# CLI
URL: https://proofkit.proof.sh/docs/fmodata/cli
Run fmodata operations from the command line — queries, scripts, webhooks, metadata, and schema changes
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { PackageInstall } from "@/components/PackageInstall";
The `@proofkit/fmodata` package ships a built-in CLI binary called **`fmodata`**. It exposes every library operation — querying records, running scripts, managing webhooks, inspecting metadata, and modifying schema — as a non-interactive command suitable for scripting, CI pipelines, and quick one-off database operations.
## Installation
The binary is included automatically when you install the package:
If you want it available globally:
## Connection Configuration
All commands share the same global connection options. Each flag has an environment variable fallback so you can set credentials once and run many commands.
| Flag | Env var | Description |
| ------------------- | -------------- | ---------------------------------------------------- |
| `--server ` | `FM_SERVER` | FileMaker Server URL (e.g. `https://fm.example.com`) |
| `--database ` | `FM_DATABASE` | Database filename (e.g. `MyApp.fmp12`) |
| `--username ` | `FM_USERNAME` | FileMaker account username |
| `--password ` | `FM_PASSWORD` | FileMaker account password |
| `--api-key ` | `OTTO_API_KEY` | OttoFMS API key (preferred over username/password) |
When both `--api-key` and `--username` are present, the API key is used. If an API key is present, missing `FM_PASSWORD` does not block authentication.
**Example — using environment variables:**
```bash
export FM_SERVER=https://fm.example.com
export FM_DATABASE=MyApp.fmp12
export OTTO_API_KEY=otto_...
fmodata metadata tables
```
**Example — passing flags directly:**
```bash
fmodata --server https://fm.example.com \
--database MyApp.fmp12 \
--api-key otto_... \
metadata tables
```
## Output Formats
By default all commands print **JSON** to stdout. Add `--pretty` to render results as a human-readable ASCII table instead.
```bash
# JSON (default)
fmodata metadata tables
# ASCII table
fmodata metadata tables --pretty
```
Errors are written to **stderr** and the process exits with code `1`.
***
## Commands
### `records`
CRUD operations against any table.
#### `records list`
Fetch records from a table.
```bash
fmodata records list --table contacts
fmodata records list --table contacts --top 10 --skip 20
fmodata records list --table contacts --select "name,email"
fmodata records list --table contacts --where "name eq 'Alice'"
fmodata records list --table contacts --order-by "name:asc"
fmodata records list --table contacts --order-by "createdAt:desc,name:asc"
```
| Option | Description |
| -------------------- | ----------------------------------------------------------- |
| `--table ` | **Required.** Table to query |
| `--top ` | Maximum records to return |
| `--skip ` | Records to skip (for pagination) |
| `--select ` | Comma-separated field names |
| `--where ` | OData `$filter` expression |
| `--order-by ` | `field:asc` or `field:desc`, comma-separated for multi-sort |
Values passed to `--select`, `--where`, and `--order-by` are URL-encoded by the CLI before sending the request.
#### `records insert`
Insert a single record.
```bash
fmodata records insert --table contacts --data '{"name":"Alice","email":"alice@example.com"}'
```
| Option | Description |
| ---------------- | -------------------------------------------- |
| `--table ` | **Required.** Target table |
| `--data ` | **Required.** Record fields as a JSON object |
#### `records update`
Update records matching a filter (or all records if `--where` is omitted).
```bash
fmodata records update \
--table contacts \
--data '{"status":"inactive"}' \
--where "lastLogin lt 2024-01-01"
```
| Option | Description |
| ---------------- | ---------------------------------------------------- |
| `--table ` | **Required.** Target table |
| `--data ` | **Required.** Fields to update as a JSON object |
| `--where ` | OData `$filter` expression (omit to update all rows) |
#### `records delete`
Delete records matching a filter.
```bash
fmodata records delete --table contacts --where "status eq 'archived'"
```
| Option | Description |
| ---------------- | -------------------------- |
| `--table ` | **Required.** Target table |
| `--where ` | OData `$filter` expression |
Omitting `--where` from `records delete` will delete **all records** in the table.
***
### `script`
#### `script run`
Execute a FileMaker script and print the result code and return value.
```bash
# No parameter
fmodata script run MyScriptName
# String parameter
fmodata script run SendEmail --param '"hello@example.com"'
# JSON object parameter
fmodata script run ProcessOrder --param '{"orderId":"123","action":"approve"}'
```
| Option | Description |
| ---------------- | ------------------------------------------------------------- |
| `--param ` | Script parameter — parsed as JSON, falls back to plain string |
The output is a JSON object:
```json
{
"resultCode": 0,
"result": "optional-return-value"
}
```
OData does not support script names with special characters (`@`, `&`, `/`) or names beginning with a number.
***
### `webhook`
#### `webhook list`
List all webhooks registered on the database.
```bash
fmodata webhook list
fmodata webhook list --pretty
```
#### `webhook get`
Get details for a specific webhook by its numeric ID.
```bash
fmodata webhook get 42
```
#### `webhook add`
Register a new webhook on a table.
```bash
fmodata webhook add \
--table contacts \
--url https://example.com/hooks/contacts
# With field selection and custom headers
fmodata webhook add \
--table contacts \
--url https://example.com/hooks/contacts \
--select "name,email,modifiedAt" \
--header "Authorization=Bearer token123" \
--header "X-App-ID=my-app"
```
| Option | Description |
| ------------------- | -------------------------------------------------------- |
| `--table ` | **Required.** Table to monitor |
| `--url ` | **Required.** Webhook endpoint URL |
| `--select ` | Comma-separated field names to include in the payload |
| `--header ` | Custom request header in `key=value` format (repeatable) |
#### `webhook remove`
Delete a webhook by ID.
```bash
fmodata webhook remove 42
```
***
### `metadata`
#### `metadata get`
Retrieve OData metadata for the database. Optionally filter to a single table.
```bash
# Full database metadata (JSON)
fmodata metadata get
# Full database metadata (XML)
fmodata metadata get --format xml
# Single table metadata
fmodata metadata get --table Contacts
# Single table metadata (XML)
fmodata metadata get --table Contacts --format xml
```
| Option | Description |
| ------------------- | ----------------------------------- |
| `--format ` | `json` (default) or `xml` |
| `--table ` | Filter metadata to a specific table |
#### `metadata tables`
List all table names in the database. This is the quickest way to inspect what's available.
```bash
fmodata metadata tables
fmodata metadata tables --pretty
```
#### `metadata fields`
List field names for a specific table. By default returns a JSON array of field name strings.
```bash
# Field names only
fmodata metadata fields --table Contacts
# Include type, nullable, and other metadata per field
fmodata metadata fields --table Contacts --details
```
| Option | Description |
| ---------------- | ------------------------------------------------------------------------------ |
| `--table ` | **Required.** Table to inspect |
| `--details` | Include field metadata (`name`, `type`, `nullable`, and additional attributes) |
***
### `schema`
Schema modification commands are **safe by default**: without `--confirm` they perform a **dry run** and print what *would* happen without making any changes.
#### `schema list-tables`
List all tables (alias for `metadata tables`).
```bash
fmodata schema list-tables
```
#### `schema create-table`
Create a new table. The `--fields` option accepts the same JSON field definition used by the TypeScript API.
```bash
# Dry run (no changes)
fmodata schema create-table \
--name NewTable \
--fields '[{"name":"id","type":"string","primary":true},{"name":"label","type":"string"}]'
# Execute for real
fmodata schema create-table \
--name NewTable \
--fields '[{"name":"id","type":"string","primary":true},{"name":"label","type":"string"}]' \
--confirm
```
| Option | Description |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| `--name ` | **Required.** New table name |
| `--fields ` | **Required.** Array of field definitions (see [Schema Management](/docs/fmodata/schema-management)) |
| `--confirm` | Execute the operation (without this flag it's a dry run) |
#### `schema add-fields`
Add fields to an existing table.
```bash
# Dry run
fmodata schema add-fields \
--table contacts \
--fields '[{"name":"phone","type":"string","nullable":true}]'
# Execute
fmodata schema add-fields \
--table contacts \
--fields '[{"name":"phone","type":"string","nullable":true}]' \
--confirm
```
| Option | Description |
| ----------------- | -------------------------------------------------------- |
| `--table ` | **Required.** Existing table name |
| `--fields ` | **Required.** Array of field definitions |
| `--confirm` | Execute the operation (without this flag it's a dry run) |
Creating tables and adding fields require a FileMaker account with DDL (Data Definition Language) privileges. Operations will throw an error if the account lacks sufficient permissions.
***
## Using in CI / Scripts
Because all connection options accept environment variables, the CLI integrates cleanly into CI pipelines:
```bash
# GitHub Actions example
- name: Run post-deploy script
env:
FM_SERVER: ${{ secrets.FM_SERVER }}
FM_DATABASE: ${{ secrets.FM_DATABASE }}
OTTO_API_KEY: ${{ secrets.OTTO_API_KEY }}
run: |
npx fmodata script run PostDeploy --param '"${{ github.sha }}"'
```
```bash
# Quick schema check in a shell script
#!/usr/bin/env bash
set -euo pipefail
TABLES=$(fmodata metadata tables)
echo "Tables in $FM_DATABASE: $TABLES"
```
***
## Using with an AI Agent
Because `fmodata` is a standard shell tool that reads from environment variables and writes JSON to stdout, it works as a natural tool for AI coding agents (Claude Code, Claude Desktop with MCP, custom agents built on the Claude API, etc.).
### Claude Code
If your project has `@proofkit/fmodata` installed, Claude Code can run `fmodata` commands directly in the terminal during a conversation. Set your credentials in the environment first:
```bash
export FM_SERVER=https://fm.example.com
export FM_DATABASE=MyApp.fmp12
export OTTO_API_KEY=otto_...
```
Then just describe what you want in plain language:
> "List the first 5 records from the `contacts` table and show me which fields are available."
Claude will run something like:
```bash
fmodata records list --table contacts --top 5
```
…and use the JSON output to reason about the schema and answer your question.
### MCP Tool Server
You can expose `fmodata` as a set of MCP tools so any MCP-compatible host (Claude Desktop, IDEs, custom agents) can call FileMaker operations directly. Create an MCP server that shells out to the CLI:
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { execFileSync } from "node:child_process";
import { z } from "zod";
const server = new McpServer({ name: "fmodata", version: "1.0.0" });
server.tool(
"query_list",
"List records from a FileMaker table",
{
table: z.string().describe("Table name"),
where: z.string().optional().describe("OData $filter expression"),
top: z.number().optional().describe("Max records to return"),
},
async ({ table, where, top }) => {
const args = ["records", "list", "--table", table];
if (where) args.push("--where", where);
if (top) args.push("--top", String(top));
const output = execFileSync("fmodata", args, { encoding: "utf8" });
return { content: [{ type: "text", text: output }] };
},
);
server.tool(
"run_script",
"Execute a FileMaker script",
{
scriptName: z.string(),
param: z.string().optional().describe("Script parameter as JSON"),
},
async ({ scriptName, param }) => {
const args = ["script", "run", scriptName];
if (param) args.push("--param", param);
const output = execFileSync("fmodata", args, { encoding: "utf8" });
return { content: [{ type: "text", text: output }] };
},
);
```
Each `fmodata` command is atomic, stateless, and returns clean JSON — exactly the shape MCP tools expect. The agent never needs to understand OData internals; it just passes field names and filter strings as arguments.
### Giving an Agent Context
The more context an agent has about your database, the more useful it can be. Provide a brief schema description alongside the CLI:
```
You have access to the `fmodata` CLI connected to our production FileMaker database.
Key tables:
- contacts (name, email, phone, status, id_company)
- companies (name, industry, arr, id_owner)
- deals (title, stage, amount, close_date, id_contact)
Use `fmodata metadata tables` to list all tables.
Use `fmodata records list --table --top 1` to inspect a table's fields.
OData filter syntax: eq, ne, lt, gt, contains(), startswith()
```
### Safety Considerations
Grant agents only the permissions they need:
* **Read-only tasks**: restrict to `records list`, `metadata get/tables`
* **Automation tasks**: allow `records insert/update` and `script run`
* **Schema changes**: keep `schema create-table` and `schema add-fields` behind a human approval step — the `--confirm` flag exists specifically to make this easy to enforce
---
# Modifying Data
URL: https://proofkit.proof.sh/docs/fmodata/crud
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
## Insert
Insert new records with type-safe data:
```typescript
// Insert a new user
const result = await db
.from(users)
.insert({
username: "johndoe",
email: "john@example.com",
active: true,
})
.execute();
if (result.data) {
console.log("Created user:", result.data);
}
```
Fields are automatically required for insert if they use `.notNull()`. Read-only fields (including primary keys) are automatically excluded:
```typescript
const users = fmTableOccurrence("users", {
id: textField().primaryKey(), // Auto-required, but excluded from insert (primaryKey)
username: textField().notNull(), // Auto-required (notNull)
email: textField().notNull(), // Auto-required (notNull)
phone: textField(), // Optional by default (nullable)
createdAt: timestampField().readOnly(), // Excluded from insert/update
});
// TypeScript enforces: username and email are required
// TypeScript excludes: id and createdAt cannot be provided
const result = await db
.from(users)
.insert({
username: "johndoe",
email: "john@example.com",
phone: "+1234567890", // Optional
})
.execute();
```
## Update
Update records by ID or filter:
```typescript
// Update by ID
const result = await db
.from(users)
.update({ username: "newname" })
.byId("user-123")
.execute();
if (result.data) {
console.log(`Updated ${result.data.updatedCount} record(s)`);
}
// Update by ROWID
const byRowId = await db
.from(users)
.update({ username: "newname" })
.byRowId(2)
.execute();
// Update by filter (using ORM API)
import { lt, and, eq } from "@proofkit/fmodata";
const result = await db
.from(users)
.update({ active: false })
.where(lt(users.lastLogin, "2023-01-01"))
.execute();
// Complex filter example
const result = await db
.from(users)
.update({ active: false })
.where(and(eq(users.active, true), lt(users.count, 5)))
.execute();
```
All fields are optional for updates (except read-only fields which are automatically excluded). TypeScript will enforce that you can only update fields that aren't marked as read-only.
For webhook hydrate flows, use `db.from(table).get({ ROWID })` when you only have FileMaker `ROWID` metadata.
## Delete
Delete records by ID or filter:
```typescript
// Delete by ID
const result = await db.from(users).delete().byId("user-123").execute();
if (result.data) {
console.log(`Deleted ${result.data.deletedCount} record(s)`);
}
// Delete by ROWID
const byRowId = await db.from(users).delete().byRowId(2).execute();
// Delete by filter (using ORM API)
import { eq, and, lt } from "@proofkit/fmodata";
const result = await db
.from(users)
.delete()
.where(eq(users.active, false))
.execute();
// Delete with complex filters
const result = await db
.from(users)
.delete()
.where(and(eq(users.active, false), lt(users.lastLogin, "2023-01-01")))
.execute();
```
## Required and Read-Only Fields
The library automatically infers which fields are required based on field builder configuration:
* **Auto-inference:** Fields with `.notNull()` are automatically required for insert
* **Primary keys:** Fields with `.primaryKey()` are automatically read-only
* **Read-only fields:** Use `.readOnly()` to exclude fields from insert/update (e.g., timestamps, calculated fields)
* **Update flexibility:** All fields are optional for updates (except read-only fields)
```typescript
const users = fmTableOccurrence("users", {
id: textField().primaryKey(), // Auto-required, auto-readOnly (primaryKey)
username: textField().notNull(), // Auto-required (notNull)
email: textField().notNull(), // Auto-required (notNull)
status: textField(), // Optional (nullable by default)
createdAt: timestampField().readOnly(), // Read-only system field
updatedAt: timestampField(), // Optional (nullable)
});
// Insert: username and email are required
// Insert: id and createdAt are excluded (cannot be provided - read-only)
db.from(users).insert({
username: "john",
email: "john@example.com",
status: "active", // Optional
updatedAt: new Date().toISOString(), // Optional
});
// Update: all fields are optional except id and createdAt are excluded
db.from(users)
.update({
status: "active", // Optional
// id and createdAt cannot be modified (read-only)
})
.byId("user-123");
```
---
# Custom Fetch Handlers
URL: https://proofkit.proof.sh/docs/fmodata/custom-fetch-handlers
You can provide custom fetch handlers for testing or custom networking
```typescript
const customFetch = async (url, options) => {
console.log("Fetching:", url);
return fetch(url, options);
};
const result = await db.from("users").list().execute({
fetchHandler: customFetch,
});
```
---
# Entity IDs vs. Field Names
URL: https://proofkit.proof.sh/docs/fmodata/entity-ids
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
This library supports using FileMaker's internal field identifiers (FMFID) and table occurrence identifiers (FMTID) instead of names. This protects your integration from both field and table occurrence name changes.
## Why use Entity IDs?
There are some pros and cons of this, so it's important to understand how this works behind the scenes so you can make the best decision for your use case.
**Pros**:
* ✅ Entity IDs are a persistent internal identifier that doesn't change when a field's name does, which can make your API calls more resilient.
* ✅ Since each OData request is logged and query requests include your full search criteria, using entity IDs help obscure the logs to make it less obvious what your actual table and fields names are.
**Cons**:
* ⚠️ Harder to debug queries with obfuscated field/table names
* ⚠️ IDs are scoped to the FileMaker file and therefore won't work if you want to make the same query on a different file.
* 📝 Note: If you are using OttoFMS to deploy files to multiple servers, entity IDs can be relied upon as long as you have strict practice of only deploying from a central development copy and never re-creating schema in each file.
## How it works
There are 2 steps to enable this feature:
1. Define the entity IDs in your schema
2. Enable `useEntityIds` in for your request. This can be set at the database, level, schema definition, or request level.
Once enabled, this feature will feel transparent to you. Behind the scenes the library will transform the names to entity IDs in your request, and the response back to the names you specify in your schema.
### Step 1: Define Entity IDs in your schema
Define your schema with entity IDs using the `.entityId()` method on field builders and the `entityId` option in `fmTableOccurrence()`.
This step is done for you automatically when you use the `@proofkit/typegen` tool to generate your schema.
Otherwise, you can find them manually in the XML version of the `$metadata` endpoint for your database, or you can calculate them using these [custom functions](https://github.com/rwu2359/CFforID) from John Renfrew.
```typescript title="Example schema with entity IDs"
import {
fmTableOccurrence,
textField,
timestampField,
} from "@proofkit/fmodata";
// Define a table with FileMaker field IDs and table occurrence ID
const users = fmTableOccurrence(
"users",
{
id: textField().primaryKey().entityId("FMFID:12039485"),
username: textField().notNull().entityId("FMFID:34323433"),
email: textField().entityId("FMFID:12232424"),
createdAt: timestampField().readOnly().entityId("FMFID:43234355"),
},
{
entityId: "FMTID:12432533", // FileMaker table occurrence ID
},
);
```
### Step 2: Enable `useEntityIds` for your request(s)
Enable `useEntityIds` in for your request. This can be set at the database, level, schema definition, or request level.
Database Level
Schema Level
Request Level
```typescript
// Enable for all requests to this database
const db = connection.database("MyDatabase", {
useEntityIds: true,
});
```
```typescript
// Enable for all requests to this schema
const users = fmTableOccurrence(
"users",
{
id: textField().primaryKey().entityId("FMFID:12039485"),
username: textField().notNull().entityId("FMFID:34323433"),
email: textField().entityId("FMFID:12232424"),
createdAt: timestampField().readOnly().entityId("FMFID:43234355"),
},
{
entityId: "FMTID:12432533", // FileMaker table occurrence ID
useEntityIds: true,
},
);
```
```typescript
// Enable for this request only
const { data, error } = await db.from(users).list().execute({
useEntityIds: true,
});
```
The heirarchy is Database > Schema > Request. This means that if you enable at the database level, you can turn it off at the schema level, or request level.
To help you with debugging, you can also set `useEntityIds` in the `getQueryString()` method to inspect the query string with or without entity IDs.
```typescript
const queryString = db.from(users).list().getQueryString({ useEntityIds: false });
console.log(queryString); // e.g. "/users?$select=id,username,email,createdAt"
```
---
# Error Handling
URL: https://proofkit.proof.sh/docs/fmodata/errors
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
All operations return a `Result` type with either `data` or `error`. The library provides rich error types that help you handle different error scenarios appropriately.
## Basic Error Checking
```typescript
const result = await db.from(users).list().execute();
if (result.error) {
console.error("Query failed:", result.error.message);
return;
}
if (result.data) {
console.log("Query succeeded:", result.data);
}
```
## HTTP Errors
Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
```typescript
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
const result = await db.from(users).list().execute();
if (result.error) {
if (isHTTPError(result.error)) {
// TypeScript knows this is HTTPError
console.log("HTTP Status:", result.error.status);
if (result.error.isNotFound()) {
console.log("Resource not found");
} else if (result.error.isUnauthorized()) {
console.log("Authentication required");
} else if (result.error.is5xx()) {
console.log("Server error - try again later");
} else if (result.error.is4xx()) {
console.log("Client error:", result.error.statusText);
}
// Access the response body if available
if (result.error.response) {
console.log("Error details:", result.error.response);
}
}
}
```
## Network Errors
Handle network-level errors (timeouts, connection issues, etc.):
```typescript
import {
TimeoutError,
NetworkError,
RetryLimitError,
CircuitOpenError,
} from "@proofkit/fmodata";
const result = await db.from(users).list().execute();
if (result.error) {
if (result.error instanceof TimeoutError) {
console.log("Request timed out");
// Show user-friendly timeout message
} else if (result.error instanceof NetworkError) {
console.log("Network connectivity issue");
// Show offline message
} else if (result.error instanceof RetryLimitError) {
console.log("Request failed after retries");
// Log the underlying error: result.error.cause
} else if (result.error instanceof CircuitOpenError) {
console.log("Service is currently unavailable");
// Show maintenance message
}
}
```
## Validation Errors
When schema validation fails, you get a `ValidationError` with rich context:
```typescript
import { ValidationError, isValidationError } from "@proofkit/fmodata";
const result = await db.from(users).list().execute();
if (result.error) {
if (isValidationError(result.error)) {
// Access validation issues (Standard Schema format)
console.log("Validation failed for field:", result.error.field);
console.log("Issues:", result.error.issues);
console.log("Failed value:", result.error.value);
}
}
```
### Validator-Agnostic Error Handling
The library uses [Standard Schema](https://github.com/standard-schema/standard-schema) to support any validation library (Zod, Valibot, ArkType, etc.). The `ValidationError.cause` property contains the normalized Standard Schema issues array:
```typescript
import { ValidationError } from "@proofkit/fmodata";
const result = await db.from(users).list().execute();
if (result.error instanceof ValidationError) {
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
// This is validator-agnostic and works with Zod, Valibot, ArkType, etc.
console.log("Validation issues:", result.error.cause);
console.log("Issues are also available directly:", result.error.issues);
// Both point to the same array
console.log(result.error.cause === result.error.issues); // true
// Access additional context
console.log("Failed field:", result.error.field);
console.log("Failed value:", result.error.value);
// Standard Schema issues have a normalized format
result.error.issues.forEach((issue) => {
console.log("Path:", issue.path);
console.log("Message:", issue.message);
});
}
```
## OData Errors
Handle OData-specific protocol errors:
```typescript
import { ODataError, isODataError } from "@proofkit/fmodata";
const result = await db.from(users).list().execute();
if (result.error) {
if (isODataError(result.error)) {
console.log("OData Error Code:", result.error.code);
console.log("OData Error Message:", result.error.message);
console.log("OData Error Details:", result.error.details);
}
}
```
## Error Handling Patterns
### Pattern 1: Using instanceof
```typescript
import {
HTTPError,
ValidationError,
TimeoutError,
NetworkError,
} from "@proofkit/fmodata";
const result = await db.from(users).list().execute();
if (result.error) {
if (result.error instanceof TimeoutError) {
showTimeoutMessage();
} else if (result.error instanceof HTTPError) {
if (result.error.isNotFound()) {
showNotFoundMessage();
} else if (result.error.is5xx()) {
showServerErrorMessage();
}
} else if (result.error instanceof ValidationError) {
showValidationError(result.error.field, result.error.issues);
} else if (result.error instanceof NetworkError) {
showOfflineMessage();
}
}
```
### Pattern 2: Using kind property (for exhaustive matching)
```typescript
const result = await db.from(users).list().execute();
if (result.error) {
switch (result.error.kind) {
case "TimeoutError":
showTimeoutMessage();
break;
case "HTTPError":
handleHTTPError(result.error.status);
break;
case "ValidationError":
showValidationError(result.error.field, result.error.issues);
break;
case "NetworkError":
showOfflineMessage();
break;
case "ODataError":
handleODataError(result.error.code);
break;
// TypeScript ensures exhaustive matching!
}
}
```
### Pattern 3: Using type guards
```typescript
import {
isHTTPError,
isValidationError,
isODataError,
isNetworkError,
} from "@proofkit/fmodata";
const result = await db.from("users").list().execute();
if (result.error) {
if (isHTTPError(result.error)) {
// TypeScript knows this is HTTPError
console.log("Status:", result.error.status);
} else if (isValidationError(result.error)) {
// TypeScript knows this is ValidationError
console.log("Field:", result.error.field);
console.log("Issues:", result.error.issues);
} else if (isODataError(result.error)) {
// TypeScript knows this is ODataError
console.log("Code:", result.error.code);
} else if (isNetworkError(result.error)) {
// TypeScript knows this is NetworkError
console.log("Network issue:", result.error.cause);
}
}
```
## Error Properties
All errors include helpful metadata:
```typescript
if (result.error) {
// All errors have a timestamp
console.log("Error occurred at:", result.error.timestamp);
// All errors have a kind property for discriminated unions
console.log("Error kind:", result.error.kind);
// All errors have a message
console.log("Error message:", result.error.message);
}
```
## Available Error Types
* **`HTTPError`** - HTTP status errors (4xx, 5xx) with helper methods (`is4xx()`, `is5xx()`, `isNotFound()`, etc.)
* **`ODataError`** - OData protocol errors with code and details
* **`ValidationError`** - Schema validation failures with issues, schema reference, and failed value
* **`ResponseStructureError`** - Malformed API responses
* **`RecordCountMismatchError`** - When `single()` or `maybeSingle()` expectations aren't met
* **`TimeoutError`** - Request timeout (from ffetch)
* **`NetworkError`** - Network connectivity issues (from ffetch)
* **`RetryLimitError`** - Request failed after retries (from ffetch)
* **`CircuitOpenError`** - Circuit breaker is open (from ffetch)
* **`AbortError`** - Request was aborted (from ffetch)
* **`BatchTruncatedError`** - Batch operation was truncated due to an earlier error
---
# Extra Properties
URL: https://proofkit.proof.sh/docs/fmodata/extra-properties
Control which extra properties are included in the response
import { Callout } from "fumadocs-ui/components/callout";
## Include Special Columns by Default (ROWID and ROWMODID)
FileMaker provides special columns `ROWID` and `ROWMODID` that uniquely identify records and track modifications. These can be included in query responses when enabled.
Enable special columns at the database level:
```typescript
const db = connection.database("MyDatabase", {
includeSpecialColumns: true,
});
const result = await db.from(users).list().execute();
// result.data[0] will have ROWID and ROWMODID properties
```
Override at the request level:
```typescript
// Enable for this request only
const result = await db.from(users).list().execute({
includeSpecialColumns: true,
});
// Disable for this request
const result = await db.from(users).list().execute({
includeSpecialColumns: false,
});
```
Use `ROWID` to hydrate a record when a webhook payload only gives you system columns:
```typescript
const result = await db.from(users).get({ ROWID: 2 }).execute();
```
Special columns are only included when no `$select` query is applied (per OData specification). When using `.select()`, special columns are excluded even if `includeSpecialColumns` is enabled.
## OData Annotations
By default, the library automatically strips OData annotations fields (`@id` and `@editLink`) from responses. If you need these fields, you can include them by passing `includeODataAnnotations: true`:
```typescript
const result = await db.from("users").list().execute({
includeODataAnnotations: true,
});
```
---
# Overview
URL: https://proofkit.proof.sh/docs/fmodata
@proofkit/fmodata
import { Card, Cards } from "fumadocs-ui/components/card";
A strongly-typed FileMaker OData API client that provides full TypeScript type inference, runtime validation, and a fluent query builder API.
## Key Features
* **Type-safe queries** - Full TypeScript inference for field names and types
* **ORM-style API** - Fluent builder pattern with column references
* **Runtime validation** - Optional standard schema validation for data transformation
* **Relationship navigation** - Type-safe navigation and expansion of related records
* **Batch operations** - Execute multiple operations atomically
* **Schema management** - Create and modify tables and fields programmatically
* **Entity IDs** - Protect against field/table name changes using FileMaker IDs
* **Webhooks** - Easily manage webhooks on your FileMaker Server to monitor table changes
## Prerequisites
To use this library you need:
* OData service enabled on your FileMaker server
* A FileMaker account with `fmodata` privilege enabled
* (if using OttoFMS) a Data API key setup for your FileMaker account with OData enabled
## When to Use OData vs Data API
Claris has given signals that OData is the future of data access for FileMaker. It's much faster and more powerful than the Data API since it does not require the server to spin up a server-side client or maintain a session for each request. However, it's a new paradigm for thinking about how we can interact with the FileMaker server and may take some adjustment. You should not worry about continuing to use the Data API if needed, but we suggest trying OData for new projects.
Get up and running in minutes
Learn how to define your table schemas
Filter, sort, and paginate your queries
Create, read, update, and delete records
---
# Inspecting Query Strings
URL: https://proofkit.proof.sh/docs/fmodata/inspecting-queries
View the full query string without executing the request
To help you with debugging, you can inspect the full query string without executing the request using the `getQueryString()` method.
```typescript
const queryString = db
.from("users")
.list()
.select({ username: users.username, email: users.email })
.where(eq(users.active, true))
.orderBy(asc(users.username))
.top(10)
.getQueryString();
console.log(queryString);
// Output: "/users?$select=username,email&$filter=active eq true&$orderby=username&$top=10"
```
---
# API Reference
URL: https://proofkit.proof.sh/docs/fmodata/methods
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
Quick reference for all available methods and operators in `@proofkit/fmodata`.
## Query Methods
| Method | Description | Example |
| ------------------------ | --------------------------------------------- | ---------------------------------------------------------------------- |
| `list()` | Retrieve multiple records | `db.from(users).list().execute()` |
| `count()` | Count records without fetching rows | `db.from(users).count().where(eq(users.active, true)).execute()` |
| `get(id)` | Get a single record by ID | `db.from(users).get("user-123").execute()` |
| `get({ ROWID })` | Get a single record by FileMaker `ROWID` | `db.from(users).get({ ROWID: 2 }).execute()` |
| `getSingleField(column)` | Get a single field value | `db.from(users).get("user-123").getSingleField(users.email).execute()` |
| `single()` | Ensure exactly one record | `db.from(users).list().where(eq(...)).single().execute()` |
| `maybeSingle()` | Get at most one record (returns null if none) | `db.from(users).list().where(eq(...)).maybeSingle().execute()` |
## CRUD Methods
| Method | Description | Example |
| ---------------- | ------------------------------------------- | -------------------------------------------------------------------- |
| `insert(data)` | Insert a new record | `db.from(users).insert({ username: "john" }).execute()` |
| `update(data)` | Update records | `db.from(users).update({ active: true }).byId("user-123").execute()` |
| `delete()` | Delete records | `db.from(users).delete().byId("user-123").execute()` |
| `byRowId(rowId)` | Target a single record by FileMaker `ROWID` | `db.from(users).update({ active: true }).byRowId(2).execute()` |
## Query Modifiers
| Method | Description | Example |
| ------------------------- | ------------------------------------- | ---------------------------------------------------------------------- |
| `where(filter)` | Filter records | `db.from(users).list().where(eq(users.active, true)).execute()` |
| `select(fields)` | Select specific fields | `db.from(users).list().select({ username: users.username }).execute()` |
| `orderBy(...columns)` | Sort results | `db.from(users).list().orderBy(asc(users.name)).execute()` |
| `top(n)` | Limit results | `db.from(users).list().top(10).execute()` |
| `skip(n)` | Skip records (pagination) | `db.from(users).list().top(10).skip(20).execute()` |
| `count()` | Include total count with a list query | `db.from(users).list().top(10).skip(20).count().execute()` |
| `expand(table, builder?)` | Expand related records | `db.from(contacts).list().expand(users).execute()` |
| `navigate(table)` | Navigate to related table | `db.from(contacts).get("id").navigate(users).execute()` |
## Filter Operators
### Comparison Operators
| Operator | Description | Example |
| -------------------- | --------------------- | ------------------------- |
| `eq(column, value)` | Equal to | `eq(users.active, true)` |
| `ne(column, value)` | Not equal to | `ne(users.role, "admin")` |
| `gt(column, value)` | Greater than | `gt(users.age, 18)` |
| `gte(column, value)` | Greater than or equal | `gte(users.score, 100)` |
| `lt(column, value)` | Less than | `lt(users.age, 65)` |
| `lte(column, value)` | Less than or equal | `lte(users.score, 0)` |
### String Operators
| Operator | Description | Example |
| --------------------------------- | --------------------- | -------------------------------------- |
| `contains(column, value)` | Contains substring | `contains(users.name, "John")` |
| `startsWith(column, value)` | Starts with | `startsWith(users.email, "admin")` |
| `endsWith(column, value)` | Ends with | `endsWith(users.email, ".com")` |
| `matchesPattern(column, pattern)` | Matches regex pattern | `matchesPattern(users.name, "^A.*e$")` |
### Array Operators
| Operator | Description | Example |
| ---------------------------- | ------------------ | ------------------------------------------------- |
| `inArray(column, values)` | Value in array | `inArray(users.role, ["admin", "moderator"])` |
| `notInArray(column, values)` | Value not in array | `notInArray(users.status, ["banned", "deleted"])` |
### Null Operators
| Operator | Description | Example |
| ------------------- | ----------- | ------------------------- |
| `isNull(column)` | Is null | `isNull(users.deletedAt)` |
| `isNotNull(column)` | Is not null | `isNotNull(users.email)` |
### Logical Operators
| Operator | Description | Example |
| ----------------- | ----------- | ---------------------------------------------------------- |
| `and(...filters)` | Logical AND | `and(eq(users.active, true), gt(users.age, 18))` |
| `or(...filters)` | Logical OR | `or(eq(users.role, "admin"), eq(users.role, "moderator"))` |
| `not(filter)` | Logical NOT | `not(eq(users.active, false))` |
### String Transform Functions
| Function | Description | Example |
| ----------------- | ---------------------------------- | --------------------------------- |
| `tolower(column)` | Convert to lowercase | `eq(tolower(users.name), "john")` |
| `toupper(column)` | Convert to uppercase | `eq(toupper(users.name), "JOHN")` |
| `trim(column)` | Remove leading/trailing whitespace | `eq(trim(users.name), "John")` |
String transforms can be nested and used with any operator:
```typescript
// Nested transforms
eq(tolower(trim(users.name)), "john")
// With other operators
contains(tolower(users.name), "john")
startsWith(toupper(users.email), "ADMIN")
```
## Sort Helpers
| Helper | Description | Example |
| -------------- | ---------------- | -------------------------- |
| `asc(column)` | Ascending order | `orderBy(asc(users.name))` |
| `desc(column)` | Descending order | `orderBy(desc(users.age))` |
## Webhook Methods
| Method | Description | Example |
| ------------------------------ | ------------------------- | -------------------------------------------------------------- |
| `webhook.add(config)` | Add a webhook | `db.webhook.add({ webhook: "https://...", tableName: users })` |
| `webhook.list()` | List all webhooks | `db.webhook.list()` |
| `webhook.get(id)` | Get a webhook by ID | `db.webhook.get(1)` |
| `webhook.remove(id)` | Remove a webhook | `db.webhook.remove(1)` |
| `webhook.invoke(id, options?)` | Manually invoke a webhook | `db.webhook.invoke(1, { rowIDs: [1, 2, 3] })` |
## Schema Methods
| Method | Description | Example |
| ---------------------------------- | --------------------- | --------------------------------------------- |
| `schema.createTable(name, fields)` | Create a new table | `db.schema.createTable("users", fields)` |
| `schema.addFields(table, fields)` | Add fields to a table | `db.schema.addFields("users", newFields)` |
| `schema.deleteTable(name)` | Delete a table | `db.schema.deleteTable("old_table")` |
| `schema.deleteField(table, field)` | Delete a field | `db.schema.deleteField("users", "old_field")` |
| `schema.createIndex(table, field)` | Create an index | `db.schema.createIndex("users", "email")` |
| `schema.deleteIndex(table, field)` | Delete an index | `db.schema.deleteIndex("users", "email")` |
## Script Methods
| Method | Description | Example |
| --------------------------- | -------------------------- | ---------------------------------------------------- |
| `runScript(name, options?)` | Execute a FileMaker script | `db.runScript("MyScript", { scriptParam: "value" })` |
## Batch Methods
| Method | Description | Example |
| ------------------- | --------------------------- | -------------------------------------- |
| `batch(operations)` | Execute multiple operations | `db.batch([query1, query2]).execute()` |
## Field Builders
| Builder | FileMaker Type | Chainable Methods |
| ------------------ | -------------- | ---------------------------------------------------------------------------------------------------- |
| `textField()` | Text | `.primaryKey()`, `.notNull()`, `.readOnly()`, `.entityId()`, `.readValidator()`, `.writeValidator()` |
| `numberField()` | Number | Same as above |
| `dateField()` | Date | Same as above |
| `timeField()` | Time | Same as above |
| `timestampField()` | Timestamp | Same as above |
| `containerField()` | Container | Same as above |
| `calcField()` | Calculation | Same as above |
## Error Types
| Error Type | Description | Type Guard |
| --------------------- | ----------------------------- | ----------------------------- |
| `HTTPError` | HTTP status errors (4xx, 5xx) | `isHTTPError()` |
| `ODataError` | OData protocol errors | `isODataError()` |
| `ValidationError` | Schema validation failures | `isValidationError()` |
| `TimeoutError` | Request timeout | `instanceof TimeoutError` |
| `NetworkError` | Network connectivity issues | `instanceof NetworkError` |
| `RetryLimitError` | Request failed after retries | `instanceof RetryLimitError` |
| `CircuitOpenError` | Circuit breaker is open | `instanceof CircuitOpenError` |
| `BatchTruncatedError` | Batch operation truncated | `isBatchTruncatedError()` |
---
# Querying Data
URL: https://proofkit.proof.sh/docs/fmodata/queries
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
## Basic Queries
Use `list()` to retrieve multiple records:
```typescript
// Get all users
const result = await db.from(users).list().execute();
if (result.data) {
result.data.forEach((user) => {
console.log(user.username);
});
}
```
Get a specific record by ID:
```typescript
const result = await db.from(users).get("user-123").execute();
if (result.data) {
console.log(result.data.username);
}
```
Get a single field value:
```typescript
const result = await db
.from(users)
.get("user-123")
.getSingleField(users.email)
.execute();
if (result.data) {
console.log(result.data); // "user@example.com"
}
```
## Filtering
fmodata provides type-safe filter operations that prevent common errors at compile time. Use the ORM-style API with operators and column references:
```typescript
import { eq, gt, and, or, contains } from "@proofkit/fmodata";
// Simple equality
const result = await db
.from(users)
.list()
.where(eq(users.active, true))
.execute();
// Comparison operators
const result = await db.from(users).list().where(gt(users.age, 18)).execute();
// String operators
const result = await db
.from(users)
.list()
.where(contains(users.name, "John"))
.execute();
// Combine with AND
const result = await db
.from(users)
.list()
.where(and(eq(users.active, true), gt(users.age, 18)))
.execute();
// Combine with OR
const result = await db
.from(users)
.list()
.where(or(eq(users.role, "admin"), eq(users.role, "moderator")))
.execute();
```
### Available Operators
**Comparison:**
* `eq()` - Equal to
* `ne()` - Not equal to
* `gt()` - Greater than
* `gte()` - Greater than or equal to
* `lt()` - Less than
* `lte()` - Less than or equal to
**String:**
* `contains()` - Contains substring
* `startsWith()` - Starts with
* `endsWith()` - Ends with
* `matchesPattern()` - Matches regex pattern
**Array:**
* `inArray()` - Value in array
* `notInArray()` - Value not in array
**Null:**
* `isNull()` - Is null
* `isNotNull()` - Is not null
**Logical:**
* `and()` - Logical AND
* `or()` - Logical OR
* `not()` - Logical NOT
**String Transforms:**
* `tolower()` - Convert to lowercase for comparison
* `toupper()` - Convert to uppercase for comparison
* `trim()` - Remove leading/trailing whitespace
## Sorting
Sort results using `orderBy()` with column references:
```typescript
import { asc, desc } from "@proofkit/fmodata";
// Single field (ascending by default)
const result = await db.from(users).list().orderBy(users.name).execute();
// Single field with explicit direction
const result = await db.from(users).list().orderBy(asc(users.name)).execute();
const result = await db.from(users).list().orderBy(desc(users.age)).execute();
// Multiple fields (variadic)
const result = await db
.from(users)
.list()
.orderBy(asc(users.lastName), desc(users.firstName))
.execute();
// Multiple fields (array syntax)
const result = await db
.from(users)
.list()
.orderBy([
[users.lastName, "asc"],
[users.firstName, "desc"],
])
.execute();
```
## Pagination
Control the number of records returned and pagination:
```typescript
// Limit results
const result = await db.from(users).list().top(10).execute();
// Skip records (pagination)
const result = await db.from(users).list().top(10).skip(20).execute();
// Count total records without fetching rows
const total = await db.from(users).count().execute();
// Fetch a page of rows and the total count in one request
const page = await db.from(users).list().top(10).skip(20).count().execute();
```
## Selecting Fields
Select specific fields to return using column references:
```typescript
// Using column references (type-safe, supports renaming)
const result = await db
.from(users)
.list()
.select({
username: users.username,
email: users.email,
userId: users.id, // Renamed from "id" to "userId"
})
.execute();
// result.data[0] will only have username, email, and userId fields
```
## Single Records
Use `single()` to ensure exactly one record is returned (returns an error if zero or multiple records are found):
```typescript
const result = await db
.from(users)
.list()
.where(eq(users.email, "user@example.com"))
.single()
.execute();
if (result.data) {
// result.data is a single record, not an array
console.log(result.data.username);
}
```
Use `maybeSingle()` when you want at most one record (returns `null` if no record is found, returns an error if multiple records are found):
```typescript
const result = await db
.from(users)
.list()
.where(eq(users.email, "user@example.com"))
.maybeSingle()
.execute();
if (result.data) {
// result.data is a single record or null
console.log(result.data?.username);
} else {
// No record found - result.data would be null
console.log("User not found");
}
```
**Difference between `single()` and `maybeSingle()`:**
* `single()` - Requires exactly one record. Returns an error if zero or multiple records are found.
* `maybeSingle()` - Allows zero or one record. Returns `null` if no record is found, returns an error only if multiple records are found.
## Chaining Methods
All query methods can be chained together:
```typescript
const result = await db
.from(users)
.list()
.select({
username: users.username,
email: users.email,
age: users.age,
})
.where(gt(users.age, 18))
.orderBy(asc(users.username))
.top(10)
.skip(0)
.execute();
```
---
# Quick Start
URL: https://proofkit.proof.sh/docs/fmodata/quick-start
import { Steps, Step } from "fumadocs-ui/components/steps";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "fumadocs-ui/components/tabs";
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
import { CliCommand } from "@/components/CliCommand";
import { AgentCommand } from "@/components/AgentCommand";
import { PackageInstall } from "@/components/PackageInstall";
import { Badge } from "@/components/ui/badge";
Here's a minimal example to get you started with `@proofkit/fmodata`:
### Install the package
#### AI Agent Integration
If you use an AI coding agent, run this command to map ProofKit's built-in skills to your project. See the [TanStack Intent docs](https://tanstack.com/intent/latest/docs/getting-started/quick-start-consumers) for more details.
### Create a server connection
Create a connection to your FileMaker server using either username/password or API key authentication (requires OttoFMS 4.11+):
Username/Password
API key
```typescript title="connection.ts"
import { FMServerConnection } from "@proofkit/fmodata";
export const connection = new FMServerConnection({
serverUrl: process.env.FM_SERVER,
auth: {
username: process.env.FM_USERNAME,
password: process.env.FM_PASSWORD,
},
});
```
```typescript title="connection.ts"
import { FMServerConnection } from "@proofkit/fmodata";
export const connection = new FMServerConnection({
serverUrl: process.env.FM_SERVER,
auth: {
apiKey: process.env.OTTO_API_KEY,
},
});
```
### Define your table schema
Automatically
Recommended
Manually
Run this command in your project to launch a browser-based UI for configuring your schema definitions. You will need environment variables set for your FileMaker server and database.
Learn more about the [@proofkit/typegen](/docs/typegen) tool.
Use field builders to create type-safe table schemas:
```typescript title="schema.ts"
import {
fmTableOccurrence,
textField,
numberField,
} from "@proofkit/fmodata";
import { z } from "zod/v4";
const users = fmTableOccurrence("users", {
id: textField().primaryKey(),
username: textField().notNull(),
email: textField().notNull(),
active: numberField()
.readValidator(z.coerce.boolean())
.writeValidator(z.boolean().transform((v) => (v ? 1 : 0))),
});
```
### Create a database instance and query data
Connect to your database and start querying:
```typescript title="query.ts"
import { eq } from "@proofkit/fmodata";
import { connection } from "./connection";
import { users } from "./schema";
const db = connection.database(process.env.FM_DATABASE);
// Query all users
const { data, error } = await db.from(users).list().execute();
if (error) {
console.error(error);
return;
}
if (data) {
console.log(data); // Array of users, properly typed
}
// Filter active users
const activeUsers = await db
.from(users)
.list()
.where(eq(users.active, true))
.execute();
```
`connection.database()` also accepts `normalizeDatabaseName` (default `true`). Standard OData requests strip a trailing `.fmp12`, while webhook create/list/remove requests ensure `.fmp12` is present.
---
# Related Data
URL: https://proofkit.proof.sh/docs/fmodata/relationships
How to retrieve related data from your FileMaker database
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
## FileMaker Relationships vs OData Navigation
When FileMaker server parses your relationship graph for relationships, it can only create basic navigation paths between table occurences where the fields are equal to each other. Sorting via a relationship is not supported and other comparison operators (if used) will be ignored and everything will be treated as a simple equality match between the fields. For complex queries or sorting, you should use the other methods of OData.
### Navigating vs Expanding
When you navigate to a related table, are essentially changing the context of your query to the related table, but with a filtered subset of records related to the parent record(s). This is most often done if you know the primary key ID of the parent record and only need the children records.
For example the query: `/users('123')/orders` will return all the orders for the user with the ID `123`. In @proofkit/fmodata, you can do this with the `navigate()` method.
Query
Result
```typescript
const result = await db.from(users).get("123").navigate(orders).execute();
```
```jsonc
// data is from the orders table; no fields from the users table will be included.
{
"data": [
{ "id": "456", "amount": 100, "date": "2021-01-01" },
{ "id": "789", "amount": 200, "date": "2021-01-02" },
]
}
```
When you use the `expand()` method, you are essentially adding the related records to the current query. This is most often done if you need to get the parent records along with the children records.
Query
Result
```typescript
const result = await db.from(users).get("123").expand(orders).execute();
```
```jsonc
// data is from the users table, along with the related orders records.
{
"data": {
"id": "123",
"name": "John Doe",
"email": "john.doe@example.com",
"orders": [
{
"id": "456",
"amount": 100,
"date": "2021-01-01"
},
{
"id": "789",
"amount": 200,
"date": "2021-01-02"
}
]
}
}
```
## Defining Navigation Paths
Define navigation paths using the `navigationPaths` option when creating table occurrences. The same navigation paths are used whether you use the `navigate()` or `expand()` methods.
These are created for you automatically when you use the `@proofkit/typegen` tool to generate your schema files.
```typescript
import { fmTableOccurrence, textField } from "@proofkit/fmodata";
const contacts = fmTableOccurrence(
"contacts",
{
id: textField().primaryKey(),
name: textField().notNull(),
userId: textField().notNull(),
},
{
navigationPaths: ["users"], // Valid navigation targets
},
);
const users = fmTableOccurrence(
"users",
{
id: textField().primaryKey(),
username: textField().notNull(),
email: textField().notNull(),
},
{
navigationPaths: ["contacts"], // Valid navigation targets
},
);
```
The `navigationPaths` option:
* Specifies which table occurrences can be navigated to from this table
* Enables runtime validation when using `expand()` or `navigate()`
* Throws descriptive errors if you try to navigate to an invalid path
## Navigating Between Tables
Navigate to related records:
```typescript
// Navigate from a specific record (using column references)
const result = await db
.from(contacts)
.get("contact-123")
.navigate(users)
.select({
username: users.username,
email: users.email,
})
.execute();
// Navigate without specifying a record first
const result = await db.from(contacts).navigate(users).list().execute();
```
## Expanding Related Records
Use `expand()` to include related records in your query results. The library validates that the target table is in the source table's `navigationPaths`:
```typescript
// Simple expand
const result = await db.from(contacts).list().expand(users).execute();
// Expand with field selection (using column references)
const result = await db
.from(contacts)
.list()
.expand(users, (b) =>
b.select({
username: users.username,
email: users.email,
}),
)
.execute();
// Expand with filtering (using ORM API)
import { eq } from "@proofkit/fmodata";
const result = await db
.from(contacts)
.list()
.expand(users, (b) => b.where(eq(users.active, true)))
.execute();
// Multiple expands
const result = await db
.from(contacts)
.list()
.expand(users, (b) => b.select({ username: users.username }))
.expand(orders, (b) => b.select({ total: orders.total }).top(5))
.execute();
```
## Nested Expands
You can nest expands to include related records of related records:
```typescript
// Nested expands
const result = await db
.from(contacts)
.list()
.expand(users, (usersBuilder) =>
usersBuilder
.select({
username: users.username,
email: users.email,
})
.expand(customers, (customerBuilder) =>
customerBuilder.select({
name: customers.name,
tier: customers.tier,
}),
),
)
.execute();
```
## Complex Expand Examples
Combine multiple options in a single expand:
```typescript
// Complex expand with multiple options
const result = await db
.from(contacts)
.list()
.expand(users, (b) =>
b
.select({
username: users.username,
email: users.email,
})
.where(eq(users.active, true))
.orderBy(asc(users.username))
.top(10)
.expand(customers, (nested) => nested.select({ name: customers.name })),
)
.execute();
```
---
# Schema Management
URL: https://proofkit.proof.sh/docs/fmodata/schema-management
import { Callout } from "fumadocs-ui/components/callout";
## Schema Management
The library provides methods for managing database schema through the `db.schema` property. You can create and delete tables, add and remove fields, and manage indexes.
### Creating Tables
```typescript
import type { Field } from "@proofkit/fmodata";
const fields: Field[] = [
{
name: "id",
type: "string",
primary: true,
maxLength: 36,
},
{
name: "username",
type: "string",
nullable: false,
unique: true,
maxLength: 50,
},
{
name: "email",
type: "string",
nullable: false,
maxLength: 255,
},
];
const tableDefinition = await db.schema.createTable("users", fields);
console.log(tableDefinition.tableName); // "users"
console.log(tableDefinition.fields); // Array of field definitions
```
### Adding Fields
```typescript
const newFields: Field[] = [
{
name: "phone",
type: "string",
nullable: true,
maxLength: 20,
},
];
const updatedTable = await db.schema.addFields("users", newFields);
```
### Deleting Tables and Fields
```typescript
// Delete an entire table
await db.schema.deleteTable("old_table");
// Delete a specific field
await db.schema.deleteField("users", "old_field");
```
### Managing Indexes
```typescript
// Create an index
const index = await db.schema.createIndex("users", "email");
console.log(index.indexName); // "email"
// Delete an index
await db.schema.deleteIndex("users", "email");
```
Schema management operations require appropriate access privileges on your FileMaker account. Operations will throw errors if you don't have the necessary permissions.
---
# Schema Definitions
URL: https://proofkit.proof.sh/docs/fmodata/schema
import { Callout } from "fumadocs-ui/components/callout";
import { TypeTable } from "fumadocs-ui/components/type-table";
import { Badge } from "@/components/ui/badge";
This library relies on a schema-first approach for good type-safety and optional runtime validation. Use **`fmTableOccurrence()`** with field builders to create your schemas. This provides full TypeScript type inference for field names in queries.
## Field Builders
Field builders provide a fluent API for defining table fields with type-safe metadata. These field types map directly to the FileMaker field types:
| Builder | FileMaker Type | Description |
| ------------------ | -------------- | -------------------- |
| `textField()` | Text | Text fields |
| `numberField()` | Number | Numeric fields |
| `dateField()` | Date | Date fields |
| `timeField()` | Time | Time fields |
| `timestampField()` | Timestamp | Date and time fields |
| `containerField()` | Container | Container fields |
| `calcField()` | Calculation | Calculated fields |
### Chainable Methods
Each field builder supports chainable methods:
| Method | Description |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| `.primaryKey()` | Mark as primary key (automatically read-only) |
| `.notNull()` | Forces the type to not be null and requires you provided a value when inserting |
| `.readOnly()` | Exclude from insert/update operations |
| `.entityId(id)` | Assign FileMaker field ID (FMFID), allowing your API calls to survive FileMaker name changes |
| `.readValidator(validator)` | Transform/validate data when reading from database (list and get operations) |
| `.writeValidator(validator)` | Transform/validate data when writing to database (insert and update operations) |
## Defining Tables
Use `fmTableOccurrence()` to define a table with field builders:
```typescript title="schema.ts"
import { z } from "zod/v4";
import {
fmTableOccurrence,
textField,
numberField,
timestampField,
} from "@proofkit/fmodata";
const contacts = fmTableOccurrence(
"contacts",
{
id: textField().primaryKey().entityId("FMFID:1"),
name: textField().notNull().entityId("FMFID:2"),
email: textField().notNull().entityId("FMFID:3"),
phone: textField().entityId("FMFID:4"), // Optional (nullable by default)
createdAt: timestampField().readOnly().entityId("FMFID:5"),
},
{
entityId: "FMTID:100", // Optional: FileMaker table occurrence ID
defaultSelect: "schema", // Optional: "all", "schema", or function. Defaults to "schema".
navigationPaths: ["users"], // Optional: valid navigation targets
},
);
```
The function returns a table object that can be used in queries and operations. To reference fields in a table, use the column references (e.g. in select and filter operations use `contacts.id`, `contacts.name`). Or use the object direclty to reference the table (such as in from, navigate, and expand operations).
## Default Field Selection
What should happen when you don't specify a `select` in your query? You can configure this at the table definition level using the `defaultSelect` option.
### Option 1: `"schema"` Default
With this default option, the library will ensure there is always a `$select` parameter when making the request to FileMaker. The `$select` parameter will only include the fields that are defined in the schema, so you'll never get back any extra fields that are not defined in your schema.
### Option 2: `"all"`
This option turns off the library behavior and will revert to FileMaker's default behavior. All non-container fields will be returned at runtime, but you will only get type information for the fields that are defined in the schema.
```typescript
const users = fmTableOccurrence(
"users",
{
/* fields */
},
{
defaultSelect: "all",
},
);
```
### Option 3: Custom Select
You can also provide a function that returns a custom select object. This function will be called with the table object and should return a custom select object.
```typescript
const users = fmTableOccurrence(
"users",
{
/* fields */
},
{
defaultSelect: (cols) => ({
username: cols.username,
email: cols.email,
}), // Only select these fields by default
},
);
```
When you call `list()` or `get()`, the `defaultSelect` is applied automatically. You can still override with explicit `select()`:
```typescript
const result = await db
.from(users)
.list()
.select({ username: users.username, email: users.email, age: users.age }) // Always overrides at the per-request level
.execute();
```
## Read/Write Validators
You can use Standard Schema validators to transform and validate data when reading from or writing to the database. We use Zod in our examples, but any other validation library that supports Standard Schema (Zod, Valibot, ArkType, etc.) will also work.
```typescript
import { z } from "zod/v4";
const users = fmTableOccurrence("users", {
id: textField().primaryKey(),
username: textField().notNull(),
email: textField().notNull(),
active: numberField()
.readValidator(z.coerce.boolean()) // Convert 1/0 to true/false when reading
.writeValidator(z.boolean().transform((v) => (v ? 1 : 0))), // Convert true/false to 1/0 when writing
});
```
The validators must transform to/from the FileMaker data type. In the example above, the write validator would have thrown a type error if the result of the `transform` was a string.
## Required and Read-Only Fields
The library automatically infers which fields are required based on field builder configuration:
* Fields with `.notNull()` are automatically required for insert
* Fields with `.readOnly()` are excluded from insert/update operations
* Fields with `.primaryKey()` are automatically read-only
```typescript
const users = fmTableOccurrence("users", {
id: textField().primaryKey(), // Auto-required, but excluded from insert (primaryKey)
username: textField().notNull(), // Auto-required (notNull)
email: textField().notNull(), // Auto-required (notNull)
phone: textField(), // Optional by default (nullable)
createdAt: timestampField().readOnly(), // Excluded from insert/update
});
// TypeScript enforces: username and email are required
// TypeScript excludes: id and createdAt cannot be provided
const result = await db.from(users).insert({
username: "johndoe",
email: "john@example.com",
phone: "+1234567890", // Optional
});
```
---
# Running Scripts
URL: https://proofkit.proof.sh/docs/fmodata/scripts
Execute FileMaker scripts via OData
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
## Simple Script Execution
```typescript
// Simple script execution
const result = await db.runScript("MyScriptName");
console.log(result.resultCode); // Script result code
console.log(result.result); // Optional script result string
```
OData doesn't support script names with special characters (e.g., `@`, `&`, `/`) or script names beginning with a number.
## Passing Parameters
Pass parameters to scripts:
```typescript
// Pass parameters to script
const result = await db.runScript("MyScriptName", {
scriptParam: "some value",
});
// Script parameters can be strings, numbers, or objects
const result = await db.runScript("ProcessOrder", {
scriptParam: {
orderId: "12345",
action: "approve",
},
});
```
## Validating Script Results
Validate script result with a Standard Schema.
```typescript
import { z } from "zod/v4";
// NOTE: Your validator must be able to parse a string.
// See Zod codecs for how to build a jsonCodec function that does this
// https://zod.dev/codecs?id=jsonschema
const schema = jsonCodec(
z.object({
success: z.boolean(),
message: z.string(),
recordId: z.string(),
}),
);
const result = await db.runScript("CreateRecord", {
resultSchema: schema,
});
// result.result is now typed based on your schema
// An error will be thrown if the validator fails
console.log(result.result.recordId);
```
In the example above, we use a [Zod codec](https://zod.dev/codecs?id=jsonschema) helper function to parse the result into a JSON object before validating.
---
# Webhooks
URL: https://proofkit.proof.sh/docs/fmodata/webhooks
Tell your FileMaker Server to call a URL when data or schema changes
import { Callout } from "fumadocs-ui/components/callout";
import { Card } from "fumadocs-ui/components/card";
Webhooks require FileMaker Server 22.0.4 or newer.
Webhooks allow you to receive notifications when data changes in your FileMaker database. The library provides a type-safe API for managing webhooks through the `db.webhook` property.
## Adding a Webhook
Create a new webhook to monitor a table for changes:
```typescript
// Basic webhook
const result = await db.webhook.add({
webhook: "https://example.com/webhook",
tableName: contactsTable,
});
// Access the created webhook ID
console.log(result.webhookResult.webhookID);
```
```typescript
// With custom headers
const result = await db.webhook.add({
webhook: "https://example.com/webhook",
tableName: contactsTable,
headers: {
"X-Custom-Header": "value",
Authorization: "Bearer token",
},
notifySchemaChanges: true, // Notify when schema changes
});
// With field selection (using column references)
const result = await db.webhook.add({
webhook: "https://example.com/webhook",
tableName: contacts,
select: [contacts.name, contacts.email, contacts.PrimaryKey],
});
// With filtering (using filter expressions)
import { eq, gt } from "@proofkit/fmodata";
const result = await db.webhook.add({
webhook: "https://example.com/webhook",
tableName: contacts,
filter: eq(contacts.active, true),
select: [contacts.name, contacts.email],
});
// Complex filter example
const result = await db.webhook.add({
webhook: "https://example.com/webhook",
tableName: users,
filter: and(eq(users.active, true), gt(users.age, 18)),
select: [users.username, users.email],
});
```
**Webhook Configuration Properties:**
* `webhook` (required) - The URL to call when the webhook is triggered
* `tableName` (required) - The `FMTable` instance for the table to monitor
* `headers` (optional) - Custom headers to include in webhook requests
* `notifySchemaChanges` (optional) - Whether to notify on schema changes
* `select` (optional) - Field selection as a string or array of `Column` references
* `filter` (optional) - Filter expression (string or `FilterExpression`) to limit which records trigger the webhook
## Listing Webhooks
Get all webhooks configured for the database:
```typescript
const result = await db.webhook.list();
console.log(result.status); // Status of the operation
console.log(result.webhooks); // Array of webhook configurations
result.webhooks.forEach((webhook) => {
console.log(`Webhook ${webhook.webhookID}:`);
console.log(` Table: ${webhook.tableName}`);
console.log(` URL: ${webhook.webhook}`);
console.log(` Notify Schema Changes: ${webhook.notifySchemaChanges}`);
console.log(` Select: ${webhook.select}`);
console.log(` Filter: ${webhook.filter}`);
console.log(` Pending Operations: ${webhook.pendingOperations.length}`);
});
```
## Getting a Webhook
Retrieve a specific webhook by ID:
```typescript
const webhook = await db.webhook.get(1);
console.log(webhook.webhookID);
console.log(webhook.tableName);
console.log(webhook.webhook);
console.log(webhook.headers);
console.log(webhook.notifySchemaChanges);
console.log(webhook.select);
console.log(webhook.filter);
console.log(webhook.pendingOperations);
```
## Removing a Webhook
Delete a webhook by ID:
```typescript
await db.webhook.remove(1);
```
## Invoking a Webhook
Manually trigger a webhook. This is useful for testing or triggering webhooks on-demand:
```typescript
// Invoke for all rows matching the webhook's filter
await db.webhook.invoke(1);
// Invoke for specific row IDs
await db.webhook.invoke(1, { rowIDs: [63, 61] });
```
## Complete Example
Here's a complete example of setting up and managing webhooks:
```typescript
import { eq } from "@proofkit/fmodata";
// Add a webhook to monitor active contacts
const addResult = await db.webhook.add({
webhook: "https://api.example.com/webhooks/contacts",
tableName: contacts,
headers: {
"X-API-Key": "your-api-key",
},
filter: eq(contacts.active, true),
select: [contacts.name, contacts.email, contacts.PrimaryKey],
notifySchemaChanges: false,
});
const webhookId = addResult.webhookResult.webhookID;
console.log(`Created webhook with ID: ${webhookId}`);
// List all webhooks
const listResult = await db.webhook.list();
console.log(`Total webhooks: ${listResult.webhooks.length}`);
// Get the webhook we just created
const webhook = await db.webhook.get(webhookId);
console.log(`Webhook URL: ${webhook.webhook}`);
// Manually invoke the webhook for specific records
await db.webhook.invoke(webhookId, { rowIDs: [1, 2, 3] });
// Remove the webhook when done
await db.webhook.remove(webhookId);
```
Webhooks are triggered automatically by FileMaker when records matching the webhook's filter are created, updated, or deleted. The `invoke()` method allows you to manually trigger webhooks for testing or on-demand processing.
---
# Configuration (OData)
URL: https://proofkit.proof.sh/docs/typegen/config-odata
import { TypeTable } from "fumadocs-ui/components/type-table";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
The typegen tool supports OData-based type generation using the `fmodata` config type. This is configured using the `proofkit-typegen-config.jsonc` file at the root of your project.
The config key can also be an array of configs, which is useful if you need to connect to multiple databases, or with different settings for different sets of tables.
Single OData config
Multiple configs
```jsonc title="proofkit-typegen-config.jsonc"
{
"$schema": "https://proofkit.proof.sh/typegen-config-schema.json",
"config": {
"type": "fmodata",
// ... your OData config here
},
}
```
```jsonc title="proofkit-typegen-config.jsonc"
{
"$schema": "https://proofkit.proof.sh/typegen-config-schema.json",
"config": [
{
"type": "fmodata",
// ... your OData config here
},
{
"type": "fmdapi",
// ... your Data API config here
},
],
}
```
## Config options
### `type` (required)
Must be set to `"fmodata"` to use OData-based type generation.
### `configName` (optional)
An optional name for this configuration. Useful when using multiple configs to identify which config is being used.
### `path` (default: `"schema"`)
The path to the directory where the generated files will be saved.
### `reduceMetadata` (optional)
If set to `true`, reduced OData annotations will be requested from the server to reduce payload size. This will prevent comments, entity ids, and other properties from being generated.
This can also be set per-table in the `tables` array to override the top-level setting for specific tables.
### `clearOldFiles` (default: `false`)
If set to `false`, the path will not be cleared before the new files are written. Only the `client` and `generated` directories are cleared to allow for potential overrides to be kept.
This is different from the Data API config, which defaults to `true`. For OData configs, we preserve existing files by default to allow for customizations.
### `alwaysOverrideFieldNames` (default: `true`)
If set to `true` (default), field names will always be updated to match metadata, even when matching by entity ID. If set to `false`, existing field names are preserved when matching by entity ID.
This can also be set per-table in the `tables` array to override the top-level setting for specific tables.
### `envNames` (optional)
If set, will use the specified environment variable names for your OData connection.
Only use the **names** of your environment variables, not the values for security reasons.
The `envNames` object supports:
* `server`: The environment variable name for the OData server URL
* `db`: The environment variable name for the database name
* `auth`: An object with either:
* `apiKey`: The environment variable name for the API key, or
* `username` and `password`: The environment variable names for username and password
## Table options
The `tables` array in the config is where you define the tables (entity sets) that you want to generate types for. You must define at least one table in the config.
### `tableName` (required)
The entity set name (table occurrence name) to generate. This table will be included in metadata download and type generation. Must match exactly the name of an entity set in your OData service.
### `variableName` (optional)
Override the generated TypeScript variable name. The original entity set name is still used for the OData path, but you can use a different name in your TypeScript code.
For example, if your entity set is named `"Customers_Table"` but you want to use `Customers` in your code:
```jsonc
{
"tableName": "Customers_Table",
"variableName": "Customers"
}
```
### `reduceMetadata` (optional)
If undefined, the top-level setting will be used. If set to `true` or `false`, it will override the top-level `reduceMetadata` setting for this specific table.
### `alwaysOverrideFieldNames` (optional)
If undefined, the top-level setting will be used. If set to `true` or `false`, it will override the top-level `alwaysOverrideFieldNames` setting for this specific table.
## Field options
Within each table's `fields` array, you can specify field-level overrides.
### `fieldName` (required)
The field name this override applies to. Must match exactly the name of a field in the table's metadata.
### `exclude` (optional)
If set to `true`, this field will be excluded from generation entirely. Useful for fields you don't need in your TypeScript types.
### `typeOverride` (optional)
Override the inferred field type from metadata. The available options are:
* `"text"`: Treats the field as a text field
* `"number"`: Treats the field as a number field
* `"boolean"`: Treats the field as a boolean (validated with `z.coerce.boolean()`)
* `"date"`: Treats the field as a date field
* `"timestamp"`: Treats the field as a timestamp field
* `"container"`: Treats the field as a container field
* `"list"`: Treats the field as a FileMaker return-delimited list via `listField()` (defaults to `string[]`)
The typegen tool will attempt to infer the correct field type from the OData metadata. Use `typeOverride` only when you need to override the inferred type.
## Example configuration
Here's a complete example of an OData configuration:
```jsonc title="proofkit-typegen-config.jsonc"
{
"$schema": "https://proofkit.proof.sh/typegen-config-schema.json",
"config": {
"type": "fmodata",
"configName": "Production OData",
"path": "schema/odata",
"reduceMetadata": true,
"clearOldFiles": false,
"alwaysOverrideFieldNames": true,
"envNames": {
"server": "ODATA_SERVER_URL",
"db": "ODATA_DATABASE_NAME",
"auth": {
"apiKey": "ODATA_API_KEY"
}
},
"tables": [
{
"tableName": "Customers",
"variableName": "Customers",
"fields": [
{
"fieldName": "InternalID",
"exclude": true
},
{
"fieldName": "Status",
"typeOverride": "boolean"
}
]
},
{
"tableName": "Orders",
"reduceMetadata": false,
"fields": [
{
"fieldName": "OrderDate",
"typeOverride": "date"
}
]
}
]
}
}
```
---
# Configuration (Data API)
URL: https://proofkit.proof.sh/docs/typegen/config
import { TypeTable } from "fumadocs-ui/components/type-table";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
import { typegenConfig } from "@proofkit/typegen/config";
The typegen tool is configured using the `proofkit-typegen-config.jsonc` file at the root of your project.
JSONC is just JSON with comments. `@proofkit/typegen` will also work with
standard JSON files.
The config key can also be an array of configs, which is useful if you need to connect to multiple databases, or with different settings for different sets of layouts.
Single config
Multiple configs
```jsonc title="proofkit-typegen-config.jsonc"
{
"$schema": "https://proofkit.proof.sh/typegen-config-schema.json",
"config": {
// ... your config here
},
}
```
```jsonc title="proofkit-typegen-config.jsonc"
{
"$schema": "https://proofkit.proof.sh/typegen-config-schema.json",
"config": [
{
// ... your config here
},
{
// ... your other config here
},
],
}
```
## Config options
### `generateClient` (default: `true`)
If set to `false`, will only generate the zod schema and/or typescript types, but not the client files. Use this to customize the generated client, but still use the typegen tool to keep your schema up to date.
### `webviewerScriptName`
If set, will generate the client using the [@proofkit/webviewer](/docs/webviewer/package) package. This allows all calls to run via a FileMaker script rather than a network request. For more information, see the [@proofkit/webviewer](/docs/webviewer/package) documentation.
### `fmMcp`
If set, typegen uses the local FM MCP adapter only while generating schemas and clients. The generated runtime client still uses [@proofkit/webviewer](/docs/webviewer/package). If `webviewerScriptName` is not set, `fmMcp.scriptName` (or `"execute_data_api"`) is used for the generated webviewer client script name.
### `clientSuffix` (default: `"Layout"`)
The suffix to add to the client name.
For example, if the `schemaName` is `"Customers"`, the client name will be `"CustomersLayout"` and you'll import it as `import { CustomersLayout } from "./schema/client";` in your application code.
### `validator` (default: `"zod/v4"`)
The validator to use for the schema. Can be `"zod/v4"` or `"zod/v3"`. If set to `false`, only TypeScript types will be generated and no runtime validation will be performed when data is returned from the database.
### `clearOldFiles` (default: `true`)
If set to `true`, will delete all existing files in the output directory before generating new files. Useful to clean up old layouts that are no longer defined in the config.
### `path` (default: `"schema"`)
The path to the directory where the generated files will be saved.
### `envNames`
If set, will use the specified environment variable names for your FileMaker connection.
Only use the **names** of your environment variables, not the values for security reasons.
## Layout options
The `layouts` array in the config is where you define the layouts that you want to generate clients for. You must define at least one layout in the config.
### `schemaName` (required)
The name of the schema to generate. This will end up being the name of the generated client and what you'll see most throughout your codebase.
### `layoutName` (required)
The name of the layout to generate a client for. Must match exactly the name of a layout in your FileMaker database.
### `strictNumbers` (default: `false`)
If set to `true`, will force all number fields to be typed as `number | null`. This is useful if you want to ensure that all numbers are properly validated and not just strings.
By default, number fields are typed as `string | number` because FileMaker may return numbers as strings in certain cases (such as very large numbers in scientific notation or blank fields). This ensures you properly account for this in your frontend code.
We suggest only turning on `strictNumbers` if you are sure that your data will not hit these conditions or if you are also using a validator like Zod.
### `generateClient` (default: `inherit`)
Use this setting to override the `generateClient` setting from the root of the config.
### `valueLists` (default: `"ignore"`)
* `"strict"`: Will force all value list fields to be typed as the actual value from the value list. This is useful if you want to ensure that all value list fields are properly validated and not just strings.
* `"allowEmpty"`: Will show the possible values from the value list, but also allow the value to be an empty string, which is the default behavior of FileMaker.
* `"ignore"`: Any value lists defined on fields in the layout will be ignored and typed as `string`.
Even if you ignore the value lists for type purposes, the value lists will still be available in the generated schema file for use in your own code.
This setting will apply to all fields with value lists in the layout. For more granular control, override the Zod schema using the `extend` method. See the [Transformations](/docs/fmdapi/validation) page for more details.
---
# Customization
URL: https://proofkit.proof.sh/docs/typegen/customization
How to customize the generated code produced by the typegen tool
## Customize the generated clients
You'll notice that the generated clients have a strict warning that says not to ever edit them, but if you need customizations for any reason, you probably need to just write the client files yourself.
However, this doesn't mean you can't use the typegen tool. If you simply pass `generateClient: false` to the typegen tool, it will still generate the zod schema and/or typescript types, which you can use in your own files and still keep the field names up to date as your database changes.
## Customize the generated schema
If you want to change the shape of the schema validator, you **can** edit the files in the main schema directory. The only files that will be overwritten each time that you run the typegen command are in the generated and client directories (if `generateClient` is enabled). For more details on this, see the [Transformations](/docs/fmdapi/validation) page.
---
# FAQ
URL: https://proofkit.proof.sh/docs/typegen/faq
### I don't like the way the code is generated. Can I edit the generated files?
Editing the generated files (in the `client` and `generated` directories) is not reccommended as it would undermine the main benefit of being able to re-run the script at a later date when the schema changes—all your edits would be overritten. You can and should edit the files in the root of your specified directory if you need some custom overrides.
### Why are number fields typed as a `string | number`?
FileMaker may return numbers as strings in certain cases (such as very large numbers in scientific notation or blank fields). This ensures you properly account for this in your frontend code. If you wish to force all numbers to be typed as `number | null`, you can enable the `strictNumbers` flag per layout in your definition.
**WARNING:** the `strictNumbers` setting is disabled by default because it may result in false types if you are not using Zod or the auto-generated layout specific client. It works by applying a transformer to the zod schema to force all number fields to be either `number` or `null`.
### How does the code generation handle Value Lists?
Values lists are exported as their own types within the schema file, but they are not enforced within the schema by default because the actual data in the field may not be fully validated.
If you want the type to be enforced to a value from the value list, you can enable the `strictValueLists` flag per schema in your definition. This feature is only reccommended when you're also using the Zod library, as your returned data will fail the validation if the value is not in the value list.
### What about date/time/timestamp fields?
For now, these are all typed as strings. You probably want to transform these values anyway, so we keep it simple at the automated level.
### Why Zod instead of just TypeScript?
**In short:** Zod is a TypeScript-first schema declaration and validation library. When you use it, you get *runtime* validation of your data instead of just compile-time validation.
FileMaker is great for being able to change schema very quickly and easily. Yes, you probably have naming conventions in place that help protect against these changes in your web apps, but no system is perfect. Zod lets you start with the assumption that any data coming from an external API might be in a format that you don't expect and then valdiates it so that you can catch errors early. This allows the typed object that it returns to you to be much more trusted throughout your app.
**But wait, does this mean that I might get a fatal error in my production app if the FileMaker schema changes?** Yes, yes it does. This is actually what you'd want to happen. Without validating the data returned from an API, it's possible to get other unexpcted side-effects in your app that don't present as errors, which may lead to bugs that are hard to track down or inconsistencies in your data.
---
# Quick Start
URL: https://proofkit.proof.sh/docs/typegen
@proofkit/typegen
import { Tabs, TabItem } from "fumadocs-ui/components/tabs";
import { Callout } from "fumadocs-ui/components/callout";
import { File, Folder, Files } from "fumadocs-ui/components/files";
import { IconFileTypeTs } from "@tabler/icons-react";
import { CliCommand } from "@/components/CliCommand";
A utility for generating runtime validators and TypeScript files from your
own FileMaker layouts.
For the ProofKit AI workflow, TypeGen is one of the main feedback tools that helps the agent catch FileMaker field and layout mistakes before runtime. See [Hybrid App Data Access](/docs/webviewer/data-access) for how generated types fit into Web Viewer apps.
## Quick Start
Run this command to initialize `@proofkit/typegen` in your project:
## Configuring Typegen
The first time you run the command, you'll be prompted to create a config file. This is where you'll specifcy the layouts that you want to generate types for.
To see all the options available, see the [Configuration](/docs/typegen/config) page.
### Environment Variables
This tool will connect to your FileMaker solution using the `@proofkit/fmdapi` package and read the layout metadata for each layout you specify in the config file. By default it will expect the following environment variables to be set:
```bash
FM_SERVER=https://your-server.com # must start with https://
FM_DATABASE=MyFile.fmp12 # must end with .fmp12
# If using OttoFMS
OTTO_API_KEY=dk_123...abc
# otherwise (fmrest privilege must be enabled on this account)
FM_USERNAME=fmrest
FM_PASSWORD=your-password
```
If you need to use different env variable names (i.e. for multiple FileMaker connections), set the `envNames` option in the [config](/docs/typegen/config) file.
## Running Typegen
Once you have a config file setup, you can run the command to generate the types:
We suggest adding a script to your `package.json` to run this command more easily
### Example Generated Files
If your typegen config is setup with 2 layouts and the path set to `schema`, this is what the generated files will look like:
} />
} />
} />
} />
} />
} />
} />
The `client` folder is where you'll import from in other files of your app to use the layout-specific clients.
The `generated` folder is where the typegen will put the generated files. These files are auto-generated and should not be edited, since your changes would just be overwritten the next time you run an update to your schema.
The other files in the root of the `schema` folder are what will be used in the generated clients, and can be safely modifed to allow for customizations.
---
# CLI Options
URL: https://proofkit.proof.sh/docs/typegen/options
import { TypeTable } from "fumadocs-ui/components/type-table";
Most configuration can be done with the [config file](/docs/typegen/config) in your project, but some options can be set at runtime.
```bash
npx @proofkit/typegen
```
## Global Options
### `--config `
Set a custom filename/path for where the config file is located or will be created. The file name must end with either `jsonc` or `json`.
* For `generate`, this specifies where to find the config file.
* For `init`, this specifies where to create the config file.
## `generate` command
```bash
npx @proofkit/typegen generate
```
This is also the default command, so "generate" is optional. If this command is run without any config file detected, you will be prompted to create the config file (the `init` command).
### `--env-path `
Set a custom path for where your environment variables are stored.
### `--reset-overrides`
Recreate the overrides file(s), even if they already exist.
## `ui` command
```bash
npx @proofkit/typegen ui
```
Launch the typegen web interface for easy configuration.
## `init` command
```bash
npx @proofkit/typegen init
```
Use this command to create a config file in the current directory.
See [Global Options](#global-options) for `--config` usage.
---
# Typegen UI
URL: https://proofkit.proof.sh/docs/typegen/ui
import { CliCommand } from "@/components/CliCommand";
The typegen tool has a built-in web interface for editing your JSON config file and running the typegen scripts. It's helpful for making sure your environment variables are setup correctly and can help autocomplete layout/field/table names into the config file.
To launch the UI, run the following command and a browser window will open at `http://localhost:3141`:
## CLI options
The UI can be configured with the following CLI options:
### `--port `
Set the port for the UI server.
### `--config `
Set a custom filename/path for where the config file is located or will be created. The file name must end with either `jsonc` or `json`.
### `--no-open`
Don't automatically open the browser.
---
# 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
* **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
* **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
* 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
* [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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
| 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
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
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
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
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
* [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
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
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
```ts
import { fmBridge } from "@proofkit/webviewer/vite-plugins";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [fmBridge()],
});
```
### Next.js 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
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
```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
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
* **"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
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
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
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
If you followed the [ProofKit AI build workflow](/docs/ai/build-a-webviewer-app),
these steps are already done for you.
### 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
Copy the `ExecuteDataAPI` and `SendCallback` scripts from the [demo file](/fmdapi-demo.fmp12) to your own FileMaker solution.
### 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
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
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.
---
# @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
`@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
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.
If you already have a ProofKit Web Viewer project and need to install or update the FileMaker add-on manually, run `proofkit add addon webviewer` from the project root. That downloads the latest add-on from the ProofKit CDN and opens it in FileMaker; you still need to add the add-on into your FileMaker file.
{" "}
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 run
`proofkit add addon webviewer` in an existing ProofKit project, 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
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
* [`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 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
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
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 do not need ProofKit installed. They need the FileMaker runtime where the Web Viewer app is deployed.
## 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
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
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
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
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
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
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
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
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
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
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.
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.
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
* **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
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
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
Read [Architecture](/docs/webviewer/architecture) to see how the pieces connect.