# ProofKit
URL: https://proofkit.dev/docs
A collection of TypeScript libraries for building great apps
## What is ProofKit?
The ProofKit CLI aims to make web development easier for beginners and more efficient for experienced devs. It’s opinionated structure allows for more than just a starter template or bootstrapping tool, but code-mod scripts that can modify a project after the initial setup.
ProofKit is not a JavaScript framework. It’s also not the only way to build a web app. It’s simply our experience of building web apps encapsulated into a CLI tool to help you structure your project and add to it over time. Each template is just a starting point, and you are free to modify any part of it as you see fit.
Learn how to use the @proofkit/cli to build a new project in minutes.
## Core Libraries
The ProofKit CLI will automatically install these other packages for you (depending on what you choose to build), but they are published seperately if you want to use them in your own projects, without the opinionated structure of the CLI.
### @proofkit/fmdapi
A library for interacting with the FileMaker Data API. [Learn more](/docs/fmdapi)
### @proofkit/typegen
A library for generating TypeScript types and validation schemas from FileMaker layouts. [Learn more](/docs/typegen)
### @proofkit/webviewer
A library for interacting with the FileMaker WebViewer. [Learn more](/docs/webviewer)
### @proofkit/fmodata
A strongly-typed FileMaker OData API client with full TypeScript inference and runtime validation. [Learn more](/docs/fmodata) (beta)
### @proofkit/better-auth
Self-hosted auth for your web apps, backed by your FileMaker database. [Learn more](/docs/better-auth)
---
# Getting Started
URL: https://proofkit.dev/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.dev/docs/better-auth/installation
import { Callout } from "fumadocs-ui/components/callout";
import { CliCommand } from "@/components/CliCommand";
import { PackageInstall } from "@/components/PackageInstall";
This package is currently in beta. Please report any issues you find to the [GitHub repository](https://github.com/proofgeist/proofkit/issues), or reach out to us in the [Community](https://community.ottomatic.cloud/c/proofkit/13)
# 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 1a: Automated Setup with the ProofKit CLI
This automated setup expects you to have Next.js and shadcn/ui set up in your project. It will also setup various auth pages for you, but these may not work if you have a prefix configured for tailwindcss. You can use this opinionated setup to get you started, or if you need more control you should refer to the manual setup.
Run the following command to add the necessary packages and config files to your project. You will not need to follow the better-auth installation guide.
# Step 1b: 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.dev/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:
---
# Introduction
URL: https://proofkit.dev/docs/cli
Build a web-app quicker than ever before.
import { InitCommand } from "@/components/InitCommand";
import { Card } from "fumadocs-ui/components/card";
## What is ProofKit?
ProofKit is a collection of TypeScript tools and libraries for building modern web applications, with a focus on seamless integration with Claris FileMaker.
The ProofKit CLI aims to make web development easier for beginners and more efficient for experienced devs. Its opinionated structure allows for more than just a starter template or bootstrapping tool, but code-mod scripts that can modify a project after the initial setup.
Think of it as our experience building web apps encapsulated into a CLI tool to help you structure your project and add to it over time. Each template is just a starting point, and you are free to modify any part of it as you see fit.
## Core Libraries
The ProofKit CLI will automatically install these other packages for you (depending on what you choose to build), but they are published separately if you want to use them in your own projects, without the opinionated structure provided by the CLI.
A library for interacting with the FileMaker Data API.
A library for generating TypeScript types and validation schemas from FileMaker layouts.
A library for interacting with the FileMaker WebViewer.
Self-hosted auth for your web apps, backed by your FileMaker database.
## Quick Start
## Videos
- Claris Engage 2025 Presentation [Watch here](https://www.youtube.com/watch?v=V3boRDZYNgw)
- FileMaker Training Videos (Livestream recording)
- [Day 1 (Building a web app with ProofKit)](https://youtu.be/mqq8gtn07fk?si=CU9FsZeZKbxk6iwx)
- [Day 2 (Building a webviewer app)](https://youtu.be/irInOmpKCrE?si=BD1qOYGsE05LAKdY)
- Community Live with Claris (June 26, 2025) [Watch here](https://www.youtube.com/watch?v=uxB9Mp4Ipm4)
---
# What's new in v2?
URL: https://proofkit.dev/docs/cli/v2
In this major release, the ProofKit CLI has been reimagined as a more powerful tool for building web apps.
# The Highlights
### A new starting point
When you start a new app using the ProofKit CLI, the new template is based on shadcn and tailwindcss instead of Mantine. This new simple template still includes the best practices from the Proof+Geist team, but now with even more flexibility.
### A new way to add [anything]
The `proofkit add` command leverages the shadcn CLI to install any component or utility into your app, or you can use the shadcn CLI directly to install any component from any compatible library. Even better, these templates are now published right here on the docs site, so we can more easily release updates and new components that you can use right away.
### A new level of compatibility
You can now use the ProofKit CLI in an existing Next.js app, even if it wasn’t bootstrapped with the CLI, as long as it's already using shadcn. This unlocks the ability for more advanced developers to leverage the power of ProofKit, while still trying out the latest and greatest in the JavaScript ecosystem.
# Why the change?
Adding new templates to the ProofKit CLI was always a challenge, and arguably the most lacking of the whole system. The templates are meant to be just a starting place, so they needed to be generic to fit any project, yet useful enough to warrant inclusion in the binary everyone downloads to run the CLI. That wasn’t scalable. But shadcn has taken the JavaScript world by storm over the past few years, especially with the rise of AI and especially tools like v0. We needed to unlock the ability to easily add anything to your JavaScript projects, whether it was a template from Proof, shadcn, v0 (AI-generated), or from any of the other emerging UI libraries built on the shadcn CLI.
But this required a new starting point. While mantine and tailwind can be used together in the same project, they get in each other’s way. At Proof+Geist, we’ve been building new projects without ProofKit for a few months now, so it was time to bring those learnings back to the framework so we could all move more quickly. New projects will start with a new foundation based on tailwindcss. It’s easier for AI to understand, and more compatible with any UI component built for shadcn to install for you.
But the shadcn CLI isn’t just about UI components. It can install any utility or set of components into your app. Take the new `@proofkit/better-auth` package that was recently released. It’s the best way to quickly and easily add self-hosted authentication to your web app, without having to compromise on login options or pay for an external auth service. But setting it up was not easy...until now! Check out our @proofkit/better-auth [guide](/docs/better-auth/installation) for more details.
# Upgrade from v1
Unfortunately, there is not an easy path for you to upgrade projects that were created with v1 to be fully compatible with v2. But if you are able to setup tailwindcss and shadcn in your existing project, you can still use the ProofKit CLI to add new components and utilities to your app.
Basic steps:
1. Configure [tailwindcss](https://tailwindcss.com/) in your project.
- If possible, you should replace any use of the Mantine UI library with tailwind-based components. You could also configure tailwind with a prefix so as not to conflict with the Mantine classes, but not all of the new components are compatible.
2. Install [shadcn](https://ui.shadcn.com/).
3. Add/change the "ui" key in your `proofkit.json` file and set to "shadcn".
```json title="proofkit.json"
{
"ui": "shadcn"
}
```
4. Install/update the ProofKit CLI, or use the npx commands to run the CLI without needing to install it in your project
Please report any issues you find to the [GitHub
repository](https://github.com/proofgeist/proofkit/issues), or reach out to us
in the [Community](https://community.ottomatic.cloud/c/proofkit/13)
---
# Adapters
URL: https://proofkit.dev/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 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-install
@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.dev/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();
console.log(`Retrieved all ${records.length} customers`);
return records.map(customer => ({
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.dev/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:
- 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 WebViewer](/docs/webviewer)
## 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.dev/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/proofgeist/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.dev/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.dev/docs/fmdapi/quick-start
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
import { Steps, Step } from "fumadocs-ui/components/steps";
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
```package-install
@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.
### 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.dev/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.dev/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.dev/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) or [@proofkit/cli](/docs/cli) 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.dev/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.dev/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.dev/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";
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:
```bash
pnpm add @proofkit/fmodata
# or
npm install @proofkit/fmodata
```
If you want it available globally:
```bash
pnpm add -g @proofkit/fmodata
```
## 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.
```bash
# JSON (default)
fmodata metadata get
# XML
fmodata metadata get --format xml
```
| Option | Description |
|---|---|
| `--format ` | `json` (default) or `xml` |
#### `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
```
---
### `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.dev/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 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.
## 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 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.dev/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.dev/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.
```typescript tab="Database Level"
// Enable for all requests to this database
const db = connection.database("MyDatabase", {
useEntityIds: true,
});
```
```typescript tab="Schema Level"
// 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 tab="Request Level"
// 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.dev/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.dev/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,
});
```
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.dev/docs/fmodata
@proofkit/fmodata
import { Callout } from "fumadocs-ui/components/callout";
import { Card, Cards } from "fumadocs-ui/components/card";
import { CliCommand } from "@/components/CliCommand";
This library is in beta status. We don't expect the method names or arguments to change further, but please submit feedback or report issues on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
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
## AI Agent Integration
If you use an AI coding agent, install ProofKit skills for better code generation after installing the package:
## 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.dev/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.dev/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()` |
| `get(id)` | Get a single record by ID | `db.from(users).get("user-123").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()` |
## 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()` | Get total count | `db.from(users).list().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.dev/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
const result = await db.from(users).list().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.dev/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 { 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, install ProofKit skills for better code generation:
### Create a server connection
Create a connection to your FileMaker server using either username/password or API key authentication (requires OttoFMS 4.11+):
```typescript title="connection.ts" tab="Username/Password"
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" tab="API key"
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();
```
---
# Related Data
URL: https://proofkit.dev/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.
```typescript tab="Query"
const result = await db.from(users).get("123").navigate(orders).execute();
```
```jsonc tab="Result"
// 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.
```typescript tab="Query"
const result = await db.from(users).get("123").expand(orders).execute();
```
```jsonc tab="Result"
// 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.dev/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.dev/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.dev/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.dev/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.
---
# ProofKit Templates
URL: https://proofkit.dev/docs/templates
---
# Configuration (OData)
URL: https://proofkit.dev/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 `@proofkit/fmodata` package is still in beta. Some of these options may change.
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.
```jsonc title="proofkit-typegen-config.jsonc" tab="Single OData config"
{
"$schema": "https://proofkit.dev/typegen-config-schema.json",
"config": {
"type": "fmodata",
// ... your OData config here
},
}
```
```jsonc title="proofkit-typegen-config.jsonc" tab="Multiple configs"
{
"$schema": "https://proofkit.dev/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.dev/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.dev/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.
```jsonc title="proofkit-typegen-config.jsonc" tab="Single config"
{
"$schema": "https://proofkit.dev/typegen-config-schema.json",
"config": {
// ... your config here
},
}
```
```jsonc title="proofkit-typegen-config.jsonc" tab="Multiple configs"
{
"$schema": "https://proofkit.dev/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. This allows all calls to run via a FileMaker script rather than a network request. For more information, see the [@proofkit/webviewer](/docs/webviewer) documentation.
### `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.dev/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.dev/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.dev/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.
## 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.dev/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.dev/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.
---
# callFMScript
URL: https://proofkit.dev/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.
---
# fmFetch
URL: https://proofkit.dev/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.dev/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 used the [ProofKit CLI](/docs/cli) to create your project, these steps
are already done for you.
### Install both packages
```package-install
@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.
---
# Getting Started
URL: https://proofkit.dev/docs/webviewer
@proofkit/webviewer
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
## Purpose
The goal of this package is to make it easy to work with FileMaker scripts and data when building a custom webviewer integration. @proofkit/webviewer works only inside of webviewer and allows you to interact with your FileMaker solution via local scripts.
This is a **client-side** package, meant to run specifically in a FileMaker WebViewer, 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](/docs/cli/webviewer/deployment-methods) guide (from the CLI docs).
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.
## Automatic Installation
We strongly recommend using the [ProofKit CLI](/docs/cli) to setup your project. It will also walk you through a FileMaker add-on that installs the neccesary layouts, scripts and custom functions to get you started.
{" "}
This demo file is a very simplified example. To see more features, use the
[ProofKit CLI](/docs/cli) to build a new app and follow the instructions to
install the FileMaker addon.
Use your preferred package manager to install the package.
```package-install
@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`
---
# Troubleshooting
URL: https://proofkit.dev/docs/webviewer/troubleshooting
## The fmFetch promise is never resolved / callback function is never called
The most common cause for this is that you forgot to specifcy the name of the webviewer the to SendCallback script. Double-check that you have a name assigned to the webviewer on the FileMaker layout and that the correct name is being set in the script you are calling.
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.
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.
---
# Deployment
URL: https://proofkit.dev/docs/cli/guides/deployment
How to deploy your ProofKit application
import { CliCommand } from "@/components/CliCommand";
import { Callout } from "fumadocs-ui/components/callout";
ProofKit applications are designed to be deployed to [Vercel](https://vercel.com), a platform that makes it easy to deploy web applications with minimal configuration.
## Prerequisites
Before deploying your application, you'll need:
1. A [Vercel account](https://vercel.com/signup) - Free tier is available
2. Your ProofKit application ready for deployment
You don't need to install the Vercel CLI beforehand - ProofKit will automatically install it for you if it's not already present on your system.
This command is indeded to be run locally on your computer. We suggest using
Vercel's built-in GitHub integration for automatic deployments.
## Deploying Your Application
To deploy your application, run:
During the deployment process, ProofKit will:
1. Check for and install the Vercel CLI if needed
2. Ensure your Node.js version is properly configured
3. Build your application for production
4. Deploy your application to Vercel
## After Deployment
Once deployment is complete, you'll receive:
- A URL where your application is live
- A link to your project dashboard on Vercel
From the Vercel dashboard, you can:
- Configure custom domains
- Set up environment variables
- Monitor application performance
- View deployment logs
Your first deployment will create a new project in your Vercel account.
Subsequent deployments will update the same project.
## Troubleshooting
When you run `proofkit deploy`, your application will first be built locally before being deployed to Vercel. This local build process helps catch any errors before the deployment begins.
### Common Issues
#### Build Failures
If your build fails, check the terminal output for:
- Missing dependencies in `package.json`
- Syntax errors in your code
- TypeScript or ESLint errors (warnings are okay, they won't cause the build to fail)
Running `pnpm build` locally before deploying can help identify issues early
in the development process.
#### Environment Variables
Make sure all required environment variables are:
- Set up in your local `.env` file for testing
- Configured in your Vercel project settings for production
If you added any new environment variables after running the `deploy` command for the first time, you may need to manually add these new variables to your Vercel project settings.
---
# Getting Started
URL: https://proofkit.dev/docs/cli/guides/getting-started
This guide walks you through how to get up and running with a new ProofKit project.
import { Callout } from "fumadocs-ui/components/callout";
import { Step, Steps } from "fumadocs-ui/components/steps";
import InitCommand from "@/components/InitCommand"
Check out the various videos available on [this page](/docs/cli).
## Prerequisites
If you're brand new to web development, you'll want to make sure these basics are installed on your system
- [Node](https://nodejs.org/en)
- To check if you have already have Node.js installed, run `node -v` in your terminal. It will return the version number if already installed.
- If not, download and install here: [nodejs.org](https://nodejs.org/en)
- [Visual Studio Code](https://code.visualstudio.com/download) (or similar IDE)
- git (download [GitHub Desktop](https://github.com/apps/desktop) if you've never used git before)
- To check if you have git installed, run `git -v` in your terminal. It will return the version number if already installed.
- _(suggested)_ [pnpm](https://pnpm.io/installation)
- If you skip this step, just follow the guides for `npm` instead, which comes with Node
- To check if you already have pnpm installed, run `pnpm -v` in your terminal. It will return the version number if already installed.
### Data Sources
Additional prerequisites apply depending on which data source you intend to connect to.
#### FileMaker
To connect to FileMaker data, you'll need a FileMaker server (19.6+) running OttoFMS (4.7+). Your server should be accessible via port 443
Your hosted FileMaker file must have an account with the `fmrest` priviledge set enabled. During setup, you'll be asked for either the account name and password for that file, or select a Data API key previously generated from OttoFMS. (If you provide credentials, a Data API key will be created for you)
#### FileMaker Web Viewer
If you intend to build for the FileMaker Web Viewer, the same requirements apply for FileMaker Server and OttoFMS as above. You will also need to make sure that your Full Access account has the `fmurlscript` priviledge enabled so the web code can trigger FileMaker scripts, making development a lot easier. For more details, see the [WebViewer Getting Started Guide](/docs/webviewer).
#### Supabase
Coming Soon
## Creating a ProofKit Project
Now you can scaffold a new ProofKit project! Follow the prompts to setup your project and connect to your own database.
**Project Name** - must be lowercase letters, numbers, `_` and `-` characters only. This will also be the folder name where your project will get created.
**Project Type** - Either a Web App for Browsers or FileMaker Web Viewer. This section of the docs focuses mostly on the browser-based projects using Next.js. For more details about the WebViewer option, see the [WebViewer Getting Started Guide](/webviewer/getting-started).
**Server URL** - Enter the URL of your FileMaker server. It must start with https:// but can be any path on the server (such as https://myserver.com/otto/app) to make it easier to copy/paste
Next you'll be asked to select a file from your server. If your solution contains multiple files, select the file where the data lives. You'll be able to add more files and/or layouts later.
If you've already created a **Data API key** for the selected file, you can select it. Alternatively you can enter a FileMaker username and password and a Data API key will be created for you.
If you get an error while creating a Data API key, it's most often one of these causes:
- The Data API is not enabled on your FileMaker server
- The account name and/or password you entered was incorrect
- The account does not have the `fmrest` extended privilege
Now you can select a **layout** to load data from, and a name for the **schema** that will be created. This is a friendly name that you can use in your code whenever you want to access the data on this layout. Schema names must be valid JavaScript variable names and unique within each data source (FileMaker file).
For FileMaker developers, "layout" and "schema" are effectively interchangeable. What you may see referred to as "schemas" is just a more generic term for a specific set of fields (and/or related fields)
If your layout name starts with `API` or `dapi` it will appear at the top of
this list!
## Launching Your Web App
Once your project is created, you'll see a few **next steps** printed to the console.
**Open your project folder.** The rest of the commands should be run in the context of the project
```bash
cd
```
If you're not already in your code editor, you'll probably find it easier to switch to it now. You can open your project folder in Visual Studio Code and run the following commands from the integrated terminal (which by default opens with the context of the project folder, so you don't need the `cd` command)
**Start your dev server.** This command builds your web app on-the-fly so you can view it in a browser on your local computer.
{/* */}
The result of this command will display a localhost URL in your console, such as [`http://localhost:3000`](http://localhost:3000), where you can view your ProofKit app.
This is a long-running command, so sometimes you'll want to stop the dev server using `ctrl+C` so that you can enter another terminal command.
Your ProofKit app is now up and running! Now you'll want to learn how to [add more pieces](/docs/cli/guides/adding-components) to your project, or check out the [motivations](/docs/cli/guides/motivation) page to learn more about the tech stack used here so you can learn more about each underlying framework that you'll come across.
u
---
# Motivation
URL: https://proofkit.dev/docs/cli/guides/motivation
Learn more about why ProofKit was created, and how it's intended to help you.
import { Card } from "fumadocs-ui/components/card";
import { Callout } from "fumadocs-ui/components/callout";
import { Badge } from "@/components/ui/badge";
The ProofKit CLI aims to make web development easier for beginners and more efficient for experienced devs. Its _opinionated_ structure allows for more than just a starter template or bootstrapping tool, but code-mod scripts that can modify a project after the initial setup.
ProofKit is **not** a JavaScript framework. It's also **not** the only way to build a web app. It's simply our experience of building web apps encapsulated into a CLI tool to help you structure your project and add to it over time. Each template is just a starting point, and you are free to modify any part of it as you see fit.
## For the New Web Dev
The world of JavaScript and web development is vast. It can feel like you don't know where to start or that there are too many options to choose from. ProofKit is **not** another JavaScript framework, but merely a CLI with some opinions to help you get started building a new web app.
ProofKit assumes that you already have some basic programming skills, perhaps from another language or low-code tool. You are a **problem solver** who's not afraid of troubleshooting, and you probably get a little thrill when the computer finally does what you intended for it to do!
After you start a new ProofKit project, we encourage you to play around with it! Use git to commit the initial changes, then feel free to move files around, knowing you can always safely revert your changes to a known working state. We often learn best by doing, and a functional codebase is a great learning tool for you to dive into web development.
## The ProofKit Tech Stack
Here is a list of the frameworks and packages that you'll come across in a ProofKit project, the reasons why we chose them, and links to their own docs.
### Browser-based Frameworks / Packages
#### [Next.js](https://nextjs.org/docs)
A well-supported open-source React framework for full-stack web applications. This means it runs code on your frontend (in the client's web browser) and backend (on the web server, or API routes). It makes it easy to get a web app up and running with all the features that we expect from a modern web app and is used by companies of all sizes to power their online presence.
#### [Shadcn/ui](https://ui.shadcn.com/) NEW
A themeable component library for React. We use their default components and styles to build apps very quickly, but with the option of adding custom styles or themes later if needed. Shadcn/ui also includes many helpful React hooks, and they provide a Modal and Notification system to ProofKit apps.
Using the shadcn CLI, you can install any component or utility from any compatible library. This makes shadcn the best choice for the most flexible and customizable apps. Some of our favorite shadcn-compatible libraries are:
- [Kibu Ui](https://www.kibo-ui.com/)
- [reui](https://reui.io/)
- [Magic UI](https://magicui.design/)
- [tweakcn (easily theme your components)](https://tweakcn.com/)
- [A longer list of more...](https://github.com/birobirobiro/awesome-shadcn-ui)
#### [Tailwind CSS](https://tailwindcss.com/) NEW
A utility-first CSS framework for rapidly building custom user interfaces. It's used by shadcn (and related libraries) to style the components and pages in your app.
#### [Tanstack Query](https://tanstack.com/query)
Sometimes also known as "React Query"; a library for managing server state in React. It's used by ProofKit to fetch data from external sources, such as REST APIs, so we can be certain that the "shape" of our data matches what we expected during development.
#### [Zod](https://zod.dev/)
A type-safe validation library for JavaScript and TypeScript. It's used by ProofKit to validate all inputs from external sources, such as REST APIs, so we can be certain that the "shape" of our data matches what we expected during development. It's also used in form validation.
#### [next-safe-action](https://next-safe-action.dev/docs/getting-started)
A wrapper around Next.js actions that include middleware (for easily checking that a user is signed in) and input validation with zod schemas.
### Authentication
#### [Clerk](https://clerk.com/)
A hosted authentication service that is extremely easy to set up and use. It's a great choice to secure most customer-facing web apps, including built-in features such as Social logins, magic link, multi-factor authentication, and more.
#### [Better-Auth](https://better-auth.com/) (self-hosted)
For when you need a bit more flexibility or if you want to self-host the authentication layer within your existing FileMaker solution. Better-auth is a great choice for this, and the [proofkit/better-auth](/docs/better-auth) adapter allows you to use FileMaker as your backend database, including automated schema migrations to easily add any necessary tables and fields to your FileMaker file.
---
# Learning Resources
URL: https://proofkit.dev/docs/cli/guides/resources
A list of resources that can help you learn more about ProofKit.
import { Callout } from "fumadocs-ui/components/callout";
There are so many resources out there that can help you learn more about web development, far too many to list here. If any of the concepts in these docs are unfamiliar to you, use this guide to help you find the tutorials that best match your learning style.
## Web Development Basics
#### git / GitHub
Source control is key to any modern coding workflow. You'll use git to keep track of any changes that you make to the text files in your project. It's helpful while learning as you can revert to previous, known good versions of your code if you make a mistake.
There are a vast number of resources out there to help you learn git. We suggest searching for a basic git tutorial that matches your learning style (interactive, video, written, etc).
[GitHub](https://github.com/) is the most popular hosting platform for git repositories. It's free to use and a great choice for easy deployment and collaboration. If you're new to git, we suggest learning GitHub also.
#### npm / pnpm
npm ("node package manager") is the package manager for JavaScript. It's the default package manager for Node.js, and is used to install and update dependencies (third-party code used in your project).
pnpm ("performant npm") is a fast, disk space efficient package manager. It's a drop in replacement for npm and is our recommended package manager. It just takes one extra step to install, but then can be used in place of npm anywhere that you see a `npm` command.
For example, to check for available updates, run `pnpm update --interactive`.
#### Code Editor / IDE
While you can use any text editor to write code, an IDE ("integrated development environment") can make your life a lot easier. We recommend using [VS Code](https://code.visualstudio.com/) or [Cursor](https://www.cursor.com/) as your IDE. These will integrate a terminal for you to run proofkit commands, and have a native git integration to easily view the changes made to your code. Cursor is a paid IDE, but it has great AI features to help you write code faster and may valuable for asking questions about the code you're writing (or written by ProofKit) as you learn.
## JavaScript / React
In web development, you'll use a variety of HTML, JavaScript, and CSS at the core of any project. Unless those terms are completely unfamiliar to you, you can likely jump right into a React tutorial and you'll be able to pick up those concepts on the way. In particular, it's likely unneccesary to learn JavaScript before starting with React.
The [React documentation](https://react.dev/learn) is a great place to start and includes browser-based code snippets to help you learn.
If you prefer to build your own project in a code editor, or you also want to learn how React will interact with Next.js, you could also try the [React fundamentals course](https://nextjs.org/learn/react-foundations/what-is-react-and-nextjs) from Next.js.
Technically speaking, we suggest using TypeScript instead of JavaScript wherever possible. Since TypeScript is a superset of JavaScript, the only difference is that you'll get more helpful errors and warnings when you make a mistake. If you really want to learn TypeScript specifically, we suggest the free [Beginner's TypeScript Course](https://www.totaltypescript.com/tutorials/beginners-typescript) from [Total TypeScript](https://www.totaltypescript.com/).
## Next.js
If you're using ProofKit to build a browser-based app, you'll be using Next.js. Next.js is known as a "meta-framework" because it's built on top of React. It's purpose is to convert your React code into a set of files that can be served to the browser.
The [Next.js course](https://nextjs.org/learn) will help you build an app from scratch starting with an example template. However, if you've already successfully started a new project with ProofKit, you may want to jump directly to the [getting started guides](https://nextjs.org/docs/app/getting-started/layouts-and-pages) in their documentation to better understand how a Next.js app is structured.
The Next.js course will cover many topics at a more unopinionated, lower level
than ProofKit, since its designed to show the capabilities of the framework.
Topics such as writing CSS directly or using raw SQL statements to fetch data
from a database are likely not things you'll need to do in a ProofKit app.
## Advanced Topics
---
# Typegen
URL: https://proofkit.dev/docs/cli/guides/typegen
How to use the ProofKit typegen to generate types and Zod schemas
import CliCommand from "@/components/CliCommand";
import { Callout } from "fumadocs-ui/components/callout";
When your database changes, you'll need to update the types and Zod schemas used in your project. The ProofKit CLI has a built-in typegen tool to help with this.
This command is run automatically when you add a new data source or schema. Since the data returned from your data sources is validated against the Zod schemas that are generated by this tool, you might run into an error loading data if you forget to run this command after making changes to your database. Simply running this command again will re-generate the types and likely fix that error
The `typegen` command may expose other errors in your codebase. For example, if you remove a field from your table or layout that was used in your web app, you'll see an error in your app after running the command.
To find all places in your codebase that need to be updated, run the TypeScript compiler manually:
---
# CLI Commands
URL: https://proofkit.dev/docs/cli/reference/cli-commands
A comprehensive guide to ProofKit CLI commands
import { CliCommand } from "@/components/CliCommand";
import { Callout } from "fumadocs-ui/components/callout";
ProofKit comes with a powerful CLI that helps you manage your project. Here's a comprehensive list of available commands.
## Basic Commands
### Initialize a New Project
Create a new ProofKit project:
### Add Components
Add new components to your project:
This will prompt you with options to add:
- Pages
- Data Sources
- Schema
- Authentication
See [Adding Components](/docs/cli/guides/adding-components) for more details.
## Maintenance Commands
### Upgrade Components
Keep your ProofKit components up to date with the upgrade command:
This command helps you maintain your ProofKit project by:
- Updating cursor rules to the latest version
- Ensuring your project has the latest component configurations
- Synchronizing project settings with the latest ProofKit standards
Run this command periodically to ensure your project stays current with the latest ProofKit features and improvements.
### Deploy
Deploy your ProofKit application:
See [Deployment](/docs/cli/guides/deployment) for more details about deploying your application.
---
# Folder Structure
URL: https://proofkit.dev/docs/cli/reference/folder-structure
The guide that explains the purpose behind the files and folders within a ProofKit project.
import { File, Folder, Files } from 'fumadocs-ui/components/files';
import { Callout } from 'fumadocs-ui/components/callout';
import { IconBrandReact, IconBrandNodejs } from "@tabler/icons-react"
Here's a brief overview of the folder structure you should expect to see in a ProofKit project. Not all files are shown in the diagram
} />
} />
} />
} />
# Conventions
The ProofKit CLI expects a certain structure for it to be able to inject code into your project.
- Wherever you see an **internal** folder, you should never touch the code inside. The ProofKit CLI may overwrite its contents when adding a new component or switching layouts.
- Any file that begins with `slot-` is designed to be edited, but you should never rename the functions or delete the file. If you don't want to render anything in the slot, simply return null from the Slot function
## The `src` directory
This is where your application code lives. Files outside of the `src` folder are for configuation only.
## The `src/app` directory
This is the Next.js app router. Learn more at the [Next.js docs](https://nextjs.org/docs/app/building-your-application/routing), and make sure you're looking at the guides for "using the app router".
In here you'll primarily be working in the `(main)` folder. This is a seperate folder that doesn't show in your URL, but groups pages together that should use the app shell layout (navbar, header, etc).
## The `src/server` directory
Code in this folder should only ever run on the server. It's where you can write functions that access your database, or interact with other services that use a shared API key that you don't want exposed to the web browser.
- `src/server/data` is where you can put functions that interact with your database.
- `src/server/services` is where you can put functions that interact with other services.
- `src/server/safe-action.ts` is where the next-safe-action client is defined, and used as the base for all server actions.
Keep in mind that other code from the `components` and `app` directories may
also be executed on the server, such as React Server Components or when a page
is server-side rendered. But the server directory is for code that should
_only_ run on the server and can include sensitive information like API keys.
## The `src/components` directory
A folder for your application's UI components. Components may also exist in the `app` directory if they are only used on a single page, but this folder is for components that may be used across multiple pages. Subfolders may be used within this folder to group components by feature, theme, or similar.
## The `src/config` directory
A place to store app-wide configuration and types. This is where you'll find the theme settings for the Mantine components, as well as the generated types and Zod schemas used for data validation from your data sources.
The `config` directory contains a lot of generated code. Be careful not to
modify these generated files directly as they may be overwritten.
## The `src/utils` directory
A folder for helper functions to be used throughout your application.
---
# ProofKit Registry
URL: https://proofkit.dev/docs/cli/registry
A collection of ProofKit templates for building great apps
The ProofKit Registry is a collection of ProofKit templates for building great apps.
## Templates
### @proofkit/template-basic
A basic ProofKit template for building great apps.
---
# Deployment Methods
URL: https://proofkit.dev/docs/cli/webviewer/deployment-methods
Learn about the different methods for deploying WebViewer code to your FileMaker file.
There are many ways to deploy WebViewer 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 WebViewer code as **data** directly in your FileMaker file. This is the default method for most WebViewer integrations and the method used when setting up a new project with the ProofKit CLI.
| 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 WebViewer 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 WebViewer code from a seperate file, you can essentially create a mirror of the required WebViewer 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 WebViewer 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 WebViewer 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 WebViewer.
| 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 WebViewer 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 WebViewer 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 WebViewer 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 |
---
# Getting Started
URL: https://proofkit.dev/docs/cli/webviewer/getting-started
How to start a new project with the ProofKit CLI
import { CliCommand } from "@/components/CliCommand";
import { Callout } from "fumadocs-ui/components/callout";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
// import Prerequisites from "../../../components/Prerequisites.mdx";
import InitCommand from "@/components/InitCommand";
Creating a ProofKit project for a FileMaker WebViewer is extremely similar to the browser-based version as covered in the [main Getting Started guide](/docs/cli/guides/getting-started). This document will simply highlight the key differences.
## Prep your FileMaker file
- The file must be hosted on a FileMaker server running OttoFMS (4.7 or later)
- The server must have the Data API enabled.
- You must have a user account that has the `fmrest` extended privilege enabled.
- The Full Access user account must have the `fmurlscript` extended privilege enabled (Allow URLs to run FileMaker scripts)
The ProofKit CLI uses the metadata endpoint of the Data API to learn about the
layouts and fields in your file to generate type-safe code that can be used in
your webviewer. This is only for development though; once you build your web
code for production, it can interact directly with the FileMaker Pro client
and will work even offline.
In a future release, we may provide a way to scaffold a new webviewer project for use with local development, but there are other tradeoffs to consider.
## Creating a ProofKit Project
Run the following command to scaffold a new ProofKit project, choosing the `webviewer` option when prompted:
Refer to the [main Getting Started guide](/docs/cli/guides/getting-started) for more details on the prompts and options available.
## Install the ProofKit WebViewer Addon
The ProofKit CLI will automatically install the ProofKit WebViewer to your computer, but you must install it into your FileMaker file to receive the necessary tables, layouts, and scripts.
To easily install the add-on, enter Layout mode on any layout and open the "Add-ons" tab in the left pane. Click the + icon in the lower left corner, then select the "ProofKit Auth" add-on from the list.
If you don't see the add-on after initialing your project with the CLI, quit and re-open FileMaker Pro.
## Developing with the WebViewer
When you run the dev server with the `dev` command, a localhost URL will be displayed for you to see your project in a browser. **DO NOT OPEN THIS URL IN A BROWSER**
In modern FileMaker versions, the WebViewer is just as capable as any other browser, but if your users are going to exclusively be running this code in a WebViewer, you should also be developing with the the WebViewer as your browser. Plus, it's the only way to test the FileMaker interactions that you will likely build.
Thankfully, the ProofKit WebViewer Addon makes it easy to switch your webviewer from dev mode and production mode.
With your dev server running (e.g. `pnpm dev`), open a **seperate** terminal instance and run:
which will open your FileMaker file in FileMaker Pro and run the `Launch Web Viewer for Dev` script. This script will ensure that you're on the correct layout and that the WebViewer is running in dev mode—pointing to your localhost URL.
You can use the other utility scripts to build your own version of this behavior:
- **Enable WebDev Mode**: Sets a global variable to indicate that the WebViewer should be running in dev mode, then resets the WebViewer to re-apply it's calculation.
- **Disable WebDev Mode**: Clears the global variable for the WebViewer to return to production mode, then resets the WebViewer to re-apply it's calculation.
To enable the JavaScript debugging in FileMaker Web Viewers, run this command in your terminal, then restart FileMaker Pro:
## Building for Production
When you're done with development, you need to **build** your project for production and then **upload** the resulting code to your FileMaker file. Use these commands in your project's root directory:
The final command is simply a shortcut for running the `build` and `upload` commands in sequence.
The `build` command will compile your project into a single html file, which will be output to the `dist` directory.
The `upload` command will use the fmp URL protocol to call a script in your FileMaker file with the path to the html file. The FileMaker script will read the data from that file and insert it into a single-record field in the `ProofKitWV` table.
If you are working with a seperated Dev/Production environments and running migrations to deploy your code to production, the uploaded code will not transfer to your production environment. See the [Deployment Methods](/webviewer/deployment-methods) page for more details.
---
# Overview
URL: https://proofkit.dev/docs/cli/webviewer/overview
Learn about the different methods for deploying WebViewer code to your FileMaker file.
The FileMaker Web Viewer object is a powerful way to build rich, interactive experiences for your FileMaker users. In many ways, it's simpler than a traditional browser-based web app because you can host the code inside of your FileMaker file (rather than hosting on a web server) and you don't have to deal with authentication because you can rely on the FileMaker Pro client.
## FAQ
#### What is the difference between the WebViewer and the browser-based project created with the ProofKit CLI?
The primary difference is the framework used. For browser-based projects, we use [Next.js](https://nextjs.org/) and the WebViewer project uses [Vite](https://vitejs.dev/) and [Tanstack Router](https://tanstack.com/router). Next.js is a full-stack framework, meaning that it results in some code that is expected to run on a server AND some code that runs in the browser. Vite allows us to build s single bundle of code for the browser that can be store in FileMaker and if needed, we can use the FileMaker Pro client as the "server" to run private functions and "host" the code.
#### Can I build WebViewer applications using Next.js?
Yes! This is sometimes preferred if you already hae a web app and want to keep things in a single codebase, or if you're already hosting the web app and don't need the FileMaker app to work offline. Simply point your webviewer object to the URL of your existing web app or local dev server and use the [@proofkit/webviewer](/docs/webviewer) package to interact with FileMaker scripts and data.
#### Does this technique work in Web Direct?
Yes. The ProofKit WebViewer Addon includes a custom function that wraps your web code so the injected FileMaker JavaScript code will still interface with the WebViewer, even in Web Direct.
---
# Adding Components
URL: https://proofkit.dev/docs/cli/guides/adding-components
How to add new components to your ProofKit project
import { CliCommand } from "@/components/CliCommand";
After initializing a new ProofKit project, you can use the CLI to add new components and features.
The `add` is now more dynamic and may change over time. To see all available items you can add via this command, see the [full list of templates available here](/docs/templates)
The ProofKit package is installed in your project, and as a script in your `package.json`, so you can run the following command (as long as your current directory is your project root):
This will prompt you with a list of available components and features to add to your project.
## Adding a Page
ProofKit includes many page templates to help you build out your web app. Use this command to select one.
If the template you select requires data, you may be prompted to select which data source and schema to use. (If there is only one, it will be selected by default without prompting)
## Adding a Data Source
If you need to connect to a new database or FileMaker file, use this command.
For FileMaker data sources, a single "Data Source" consists of:
- A FileMaker Server
- A FileMaker File
- An OttoFMS API Key (representing a single account name with permission to the file)
If you need to change any one of these (i.e. to connect to a different file on the same server), you'll want to add a new data source.
## Adding Schema
When you want to interact with data from a new table or layout, you'll need to add a `schema`.
---
# All Templates
URL: https://proofkit.dev/docs/cli/guides/adding-components/templates
import Redirect from "@/components/redirect";
# ProofKit Templates
---
# FileMaker Add-on Auth
URL: https://proofkit.dev/docs/cli/guides/auth/add-on
How to add FileMaker Add-on Auth to your FileMaker file
import { CliCommand } from "@/components/CliCommand";
This component is no longer the recommended approach for self-hosting your web app's authentication in FileMaker. Instead, check out the [Better-Auth installation guide](/docs/better-auth/installation) for a more flexible and customizable solution.
ProofKit includes a self-hosted authentication solution that is built to be flexible and easy to customize. Rather than using a third-party library or authentication service, the ProofKit CLI installs all of the necessary functions into your project and then you just have to install the FileMaker add-on in your FileMaker file to get the necessary tables in your database.
While FileMaker Add-on Auth is built to be secure, it is more advanced than a typical third-party authentication service and does warrant a good understanding of how it works before you customize it beyond the default settings. For more details about the security principles behind it, see the [Lucia documentation](https://lucia-auth.com/).
## Installing
From within a ProofKit project, run the following command:
This auth integration requires that some specific tables and layouts exist in your FileMaker file. The CLI will detect if these layouts and not in your file, and notify you to install the FileMaker Add-on if they are missing. The FileMaker Add-on encapsulates all of the changes needed and makes it easy to add the requirements to your file.
To easily install the add-on, enter Layout mode on any layout and open the "Add-ons" tab in the left pane. Click the + icon in the lower left corner, then select the "ProofKit Auth" add-on from the list.
If you don't see the add-on after installing FileMaker Add-on Auth from the CLI, quit and re-open FileMaker Pro.
After you install the add-on for the first time, make sure you re-run the `typegen` command to add the types in your project.
## Email Verification Setup
By default, FileMaker Add-on Auth works with an email/password based login and asks users to verify their email address when they change their email and when they first sign up. However, you must implement the email sending functionality with the third-party service of your choice. If you skipped the email provider question during the setup, the email verification codes will be displayed in the terminal of your running dev server for your testing.
To customize the email sent to your users, edit the `src/server/auth/email.tsx` file. The actual email is rendered to HTML using the [React Email](https://react.email/docs/introduction) library and can be sent with any email service you choose, but ProofKit has built-in support for [Resend](https://resend.com/) and [Plunk](https://www.useplunk.com/).
## Usage
## Customizing
Make sure you fully understand the security implications of any changes you make to the FileMaker Add-on Auth code. For a full guide to the security principles behind it, see the [Lucia documentation](https://lucia-auth.com/) and [The Copenhagen Book](https://thecopenhagenbook.com/).
Once the add-on is installed in your FileMaker file, you can customize the tables and layouts to better fit your solution. For example, if you already have a users table, you may wish to use that instead of the new one created by the add-on.
The easiest way to customize is to use the tables from FileMaker Add-on and create a one-to-one relationship with your existing table(s). You can also add new fields to the FileMaker Add-on tables to extend their functionality.
After you've made your modifications, run the `typegen` command again to update the types in your project.
Then, we suggest running the `tsc` command to check for any errors in the FileMaker Add-on Auth code.
For example, if you added new fields to the session or user layouts, you will likely need to update the functions that create records in those tables and provide some default values.
All expiration timestamps are stored as number fields representing the number of milliseconds since the Unix Epoch and these are a bit easier to work with in JavaScript. Any record with an expiration timestamp in the past can be safely deleted, as it simply represents an abandonded session, email verification, or password reset request.
### Session Table
From [The Copenhagen Book](https://thecopenhagenbook.com/sessions):
> **Sessions are a way to persist state in the server.** It is especially useful for managing the authentication state, such as the client's identity. We can assign each session with a unique ID and store it on the server to use it as a token. Then the client can associate the request with a session by sending the session ID with it. To implement authentication, we can simply store user data alongside the session.
The session layout also includes related fields for the user, so user info can be fetched in a single request via the FileMaker Data API. **Unless you change the types in the code**, you should expect all data from this layout to be available to the currently logged in user in the browser.
By default, the layout for the session table should be named `proofkit_auth_sessions` but this can be customized in the fmschema.config.mjs file.
### Users Table
This table store information about a user's account, including their email address, email verification status, and password hash. You should be careful to never expose the password hash to the client, nor query this layout directly outside of the provided FileMaker Add-on Auth functions.
By default, the layout for the users table should be named `proofkit_auth_users` but this can be customized in the fmschema.config.mjs file.
### Verification Table
The purpose of this table is to store a request for a user to change their email address until they have successfully verified the new email by entering the code sent to the new address.
By default, the layout for the verification table should be named `proofkit_auth_email_verification` but this can be customized in the fmschema.config.mjs file.
### Password Reset Table
The purpose of this table is to store a request for a user to reset their password until they have successfully reset their password by entering the code sent to their email address.
By default, the layout for the password reset table should be named `proofkit_auth_password_reset` but this can be customized in the fmschema.config.mjs file.
### Alternative Authentication Methods
At this time, FileMaker Add-on Auth will only setup email/password authentication for you, but any other authentication methods can be added by modifying the code yourself. Guides for integrating OAuth providers, passkeys (WebAuthn), two-factor, and more are available in the [Lucia documentation](https://lucia-auth.com/). Just keep in mind that you'll likely need to add more fields to the tables and layouts as mentioned above to support those additional features.
---
# Clerk
URL: https://proofkit.dev/docs/cli/guides/auth/clerk
How to add Clerk authentication to your ProofKit project
import { CliCommand } from "@/components/CliCommand";
## What is Clerk?
[Clerk](https://clerk.com/) is a hosted authentication service that is extremely easy to set up and use. It's a great choice to secure most customer-facing web apps, including built-in features such as Social logins, magic link, multi-factor authentication, and more.
## Adding Clerk to your ProofKit project
To add Clerk to your ProofKit project, run the following command:
For further details on how to use Clerk, see the [Clerk documentation](https://clerk.com/docs/references/nextjs/overview).