Skip to Content
Config SystemConfiguration Reference

Configuration Reference

Complete reference for the foir.config.ts file format, all available interfaces, and helper functions.

ApplyConfigInput

The top-level config object passed to defineConfig. This is the shape of your entire config file.

interface ApplyConfigInput { key: string; // Unique identifier for this config name: string; // Display name configType?: string; // Config type identifier force?: boolean; // Force reinstall (delete and recreate) /** Base URL prepended to relative operation endpoints at push time. */ operationBaseUrl?: string; /** Project pin + declarative project-level settings. */ project?: ApplyConfigProjectInput; models?: ApplyConfigModelInput[]; // Content models operations?: ApplyConfigOperationInput[]; // Operation endpoints segments?: ApplyConfigSegmentInput[]; // Customer segments schedules?: ApplyConfigScheduleInput[]; // Cron schedules hooks?: ApplyConfigHookInput[]; // Lifecycle hooks authProviders?: ApplyConfigAuthProviderInput[]; // Auth providers relyingParties?: ApplyConfigRelyingPartyInput[]; // Login-with-Foir relying parties placements?: ApplyConfigPlacementInput[]; // Editor placements apiKeys?: ApplyConfigApiKeyInput[]; // API keys provisioned at push time customerRoles?: ApplyConfigCustomerRoleInput[]; // Customer RBAC roles (token scopes) /** Per-project app installations, keyed by app name. See /config/apps. */ apps?: Record<string, AppInput>; /** W3C design tokens document for this project. See /config/design-tokens. */ designTokens?: ApplyConfigDesignTokensInput; [key: string]: unknown; // Additional fields accepted at runtime }

All arrays/maps are optional. Include only what your project needs.

Note: The typed interfaces include a [key: string]: unknown index signature, so additional fields beyond the ones listed here are accepted at runtime and passed through to the platform API.

Helper Functions

Every helper function provides TypeScript IntelliSense and compile-time validation. They accept and return the same object, so using them is optional but recommended.

defineConfig

Wraps the top-level config object.

import { defineConfig } from '@foir/cli/configs'; export default defineConfig({ key: 'my-app', name: 'My App', models: [...], operations: [...], });

defineModel

Defines a content model with typed fields.

import { defineModel } from '@foir/cli/configs'; const page = defineModel({ key: 'page', name: 'Page', fields: [ { key: 'title', type: 'text', label: 'Title', required: true }, ], lookups: [ { keyBy: ['slug'] }, // fetch by slug; see /config/models#lookups ], });

defineField

Defines a single field definition. Useful when building fields programmatically or reusing field definitions across models.

import { defineField } from '@foir/cli/configs'; const titleField = defineField({ key: 'title', type: 'text', label: 'Title', required: true, placeholder: 'Enter a title...', queryable: true, // allow where/orderBy on this field access: { write: ['admin', 'service'] }, // field-level access control });

Set queryable: true to allow filtering and sorting on a field (the platform rejects sorts on non-queryable fields). The access object names the principals allowed to read/write the field — a non-empty write is an allow-list ("public" | "self" | "scoped" | "service" | "admin"); omit it for the default (any authenticated principal). See Defining Models.

defineSelectField

Defines a select field. Required for select fields because they need a typed optionModelKey pointing at a model whose records become the options. Plain defineField({ type: 'select' }) is rejected.

import { defineSelectField } from '@foir/cli/configs'; const status = defineSelectField({ key: 'status', type: 'select', label: 'Status', required: true, config: { optionModelKey: 'status-option', // a model with records like 'draft', 'published' multiple: false, }, });

defineEnumField

Defines an enum field — the inline-choice type. Options are declared inline as an { label, value } array. (For options drawn from another model’s records, use defineSelectField instead.)

import { defineEnumField } from '@foir/cli/configs'; const status = defineEnumField({ key: 'status', type: 'enum', label: 'Status', required: true, config: { options: [ { value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }, ], multiple: false, default: 'draft', }, });

defineOperation

Defines an HTTP endpoint for custom logic.

import { defineOperation } from '@foir/cli/configs'; const syncOp = defineOperation({ key: 'sync-data', name: 'Sync Data', description: 'Syncs records to external system', endpoint: '/sync/all', });

defineHook

Defines a lifecycle hook that triggers an operation on content events.

import { defineHook } from '@foir/cli/configs'; const hook = defineHook({ key: 'sync-on-create', name: 'Sync on create', event: 'RECORD_CREATED', operationKey: 'redirect-sync', filter: { modelKey: 'redirect' }, });

definePlacement

Defines a custom editor panel.

import { definePlacement } from '@foir/cli/configs'; const editorTab = definePlacement({ type: 'main-editor', url: '/', modelKeys: ['page'], tabName: 'Custom Editor', });

defineSchedule

Defines a cron schedule for an operation.

import { defineSchedule } from '@foir/cli/configs'; const nightly = defineSchedule({ operationKey: 'sync-data', cron: '0 2 * * *', timezone: 'America/New_York', enabled: true, });

defineSegment

Defines a customer segment with rules.

import { defineSegment } from '@foir/cli/configs'; const vips = defineSegment({ key: 'vip-customers', name: 'VIP Customers', description: 'High-value customers', rules: { type: 'condition', left: { type: 'field', path: 'totalSpend' }, operator: 'greater_than', right: { type: 'literal', value: 500 }, }, isActive: true, });

defineAuthProvider

Defines an authentication provider.

import { defineAuthProvider } from '@foir/cli/configs'; const auth = defineAuthProvider({ key: 'google-oauth', name: 'Google OAuth', type: 'oauth2', config: { clientId: process.env.GOOGLE_CLIENT_ID, allowedDomains: ['example.com'], }, enabled: true, isDefault: false, priority: 10, });

defineRelyingParty

Defines a Login-with-Foir relying party — a storefront or third-party app that can launch the hosted customer login at auth.foir.dev/authorize?client_id=… and exchange the auth code at /customer/oauth/token. foir push is project-scoped, so relying parties declared in config are customer-lane.

import { defineRelyingParty } from '@foir/cli/configs'; const storefront = defineRelyingParty({ clientId: 'eide-clothing-storefront', name: 'Eide Clothing', redirectUris: ['https://eide.clothing/auth/callback'], allowedScopes: ['records:read', 'profile:read'], loginMethods: { password: true, otp: false, disabledProviders: ['apple'], // hide these project providers on this RP only }, });
interface ApplyConfigRelyingPartyInput { kind?: 'customer' | 'admin'; // Which lane; config RPs are customer-lane clientId: string; // Public RP slug, unique per project name: string; // Display name on the hosted login redirectUris: string[]; // Allow-listed exact redirect URIs allowedScopes?: string[]; // OAuth scopes the RP can request loginMethods?: { // Per-RP login method filter (override-down only) password?: boolean; // Show password tab (default true) otp?: boolean; // Show OTP tab (default true) disabledProviders?: string[]; // Provider keys to hide on this RP }; customDomains?: string[]; // White-label login domains, e.g. ['login.acme.com'] postLoginUrl?: string; // Where a bare custom-domain login lands (front-door) allowedOrigins?: string[]; // Origins allowed a browser session, e.g. ['https://app.acme.com'] }

customDomains lets you serve the hosted login from your own domain instead of auth.foir.dev. foir push registers each domain (creating a pending row and a DNS challenge) and prunes any you remove from the list; verification is a separate step — add the CNAME and _foir-challenge TXT records the push output prints, then verify. See Custom Domains.

Registering, verifying or removing a domain needs the domains:write permission, which project admins and tenant owners hold by default. Editors and viewers do not, so a push that adds or removes a customDomains entry will fail for them while the rest of the manifest applies normally. Creating the relying party itself needs relyingparties:write, held by the same roles.

allowedOrigins lists the origins your app runs on, and is what lets a browser exchange the first-party session cookie for an access token — the backend-less SPA flow in SPA (no backend). They are exact origins, matched the same way redirectUris are: scheme and host (and port, if it isn’t 443), https only, no wildcards and no paths.

Note: There is no domain-wide default, deliberately. Granting every origin on acme.com would also grant a subdomain you no longer control, or one serving user-generated content — either of which could then mint your customers’ access tokens. If you list no origins, the browser session simply doesn’t work; the Login with Foir token flow is unaffected.

Omitting the key leaves the list untouched on push; passing [] clears it.

So relying parties, their login methods, their white-label domains, and the origins allowed a browser session are all declared in foir.config.ts alongside the rest of your project.

defineDesignTokens

Defines a W3C Design Tokens document for the project. The CLI applies it verbatim and references like {font.size.display1} are preserved on disk. See Design Tokens for the full shape.

import { defineDesignTokens } from '@foir/cli/configs'; const tokens = defineDesignTokens({ color: { brand: { primary: { $value: '#2c4433', $type: 'color' } } }, font: { size: { display1: { $value: '48px', $type: 'dimension' } } }, });

defineSecrets

Secrets are declared separately in a foir.secrets.ts file (not in foir.config.ts) and reconciled with foir secrets push. defineSecrets is the type-safe identity helper for that file:

import { defineSecrets } from '@foir/cli/configs'; export default defineSecrets({ secrets: [ { ownerKind: 'project', label: 'deepl_api_key' }, ], });

Plaintext lives in a sibling local.foir.secrets.ts (gitignored) keyed by label, or in env vars. Production never runs the reconciler — operators set production secrets through the admin UI.

Field Types

The following field types are available for model field definitions:

TypeDescription
textSingle- or multi-line text (set config.widget: 'textarea' for multi-line)
richtextTranslatable rich-text prose
flexibleOrdered array of structured blocks
numberNumeric values
booleanYes/no toggle
dateDate picker (can include time)
enumInline {label, value} choices — see defineEnumField
selectRecord-backed choice (optionModelKey) — see defineSelectField
referenceReference to another record
modelEmbedded value of an inline model
imageImage upload with alt text and focal point
videoVideo upload with poster and HLS streaming
fileGeneric file upload
listOrdered list of items (itemType controls the inner type)
jsonRaw JSON

See Field Types for detailed descriptions of each type.

Complete Example

This is the full config for the Cloudflare KV Redirect extension, showing models, operations, hooks, and placements working together:

import { defineConfig } from '@foir/cli/configs'; export default defineConfig({ key: 'cloudflare-kv', name: 'Cloudflare KV Redirector', configType: 'cloudflare-kv', models: [ { key: 'redirect', name: 'Redirect', fields: [ { key: 'displayName', type: 'text', label: 'Display Name', required: false, }, { key: 'sourcePattern', type: 'text', label: 'Source Pattern', required: true, }, { key: 'targetPattern', type: 'text', label: 'Target Pattern', required: true, }, { key: 'statusCode', type: 'enum', label: 'Status Code', required: true, config: { options: [ { value: '301', label: '301 - Permanent Redirect' }, { value: '302', label: '302 - Temporary Redirect' }, { value: '307', label: '307 - Temporary Redirect (preserve method)' }, { value: '308', label: '308 - Permanent Redirect (preserve method)' }, ], }, }, { key: 'priority', type: 'number', label: 'Priority', required: false, }, { key: 'activeFrom', type: 'date', label: 'Active From', required: false, }, { key: 'activeTo', type: 'date', label: 'Active Until', required: false, }, { key: 'reason', type: 'enum', label: 'Reason', required: false, config: { options: [ { value: 'migration', label: 'Site Migration' }, { value: 'rebrand', label: 'Rebrand / Rename' }, { value: 'campaign', label: 'Marketing Campaign' }, { value: 'discontinued', label: 'Discontinued Content' }, { value: 'seo', label: 'SEO Optimization' }, { value: 'other', label: 'Other' }, ], }, }, ], }, ], hooks: [ { key: 'sync-on-create', name: 'Sync on redirect create', event: 'RECORD_CREATED', operationKey: 'redirect-sync', filter: { modelKey: 'redirect' }, }, { key: 'sync-on-update', name: 'Sync on redirect update', event: 'RECORD_UPDATED', operationKey: 'redirect-sync', filter: { modelKey: 'redirect' }, }, { key: 'sync-on-delete', name: 'Sync on redirect delete', event: 'RECORD_DELETED', operationKey: 'redirect-sync', filter: { modelKey: 'redirect' }, }, ], operations: [ { key: 'redirect-sync', name: 'Sync Redirects to Edge', description: 'Syncs redirect records to Cloudflare KV', endpoint: '/sync/all', }, { key: 'redirect-sync-status', name: 'Get Sync Status', description: 'Returns current sync metadata for display in the editor', endpoint: '/sync/status', }, { key: 'redirect-undeploy', name: 'Undeploy Edge Redirects', description: 'Tear down Cloudflare Worker and KV namespace', endpoint: '/deploy/destroy', }, ], placements: [ { type: 'main-editor', url: '/', modelKeys: ['redirect'], hideContentTab: true, tabName: 'Redirect Editor', }, ], });

ApiKey Provisioning

apiKeys declares API keys the CLI provisions during foir push. The created key value is written into your project’s .env file under the named environment variable so your application can pick it up immediately.

interface ApplyConfigApiKeyInput { /** Display name (e.g. "Tilly iOS"). */ name: string; /** 'public' for client apps, 'secret' for server/BFF use. */ keyType: 'public' | 'secret'; /** Env var the CLI writes the key value to (e.g. "FOIR_PUBLIC_KEY"). */ envVar: string; /** Scopes — use ["*"] for full access. */ scopes?: string[]; /** Restrict to specific model keys. */ allowedModels?: string[]; /** Restrict file uploads to specific MIME types. */ allowedFileTypes?: string[]; }
export default defineConfig({ key: 'tilly', name: 'Tilly', apiKeys: [ { name: 'Tilly iOS', keyType: 'public', envVar: 'TILLY_PUBLIC_KEY', scopes: ['records:read', 'records:write:tilly_note'], allowedModels: ['tilly_note', 'tilly_block'], }, { name: 'Tilly BFF', keyType: 'secret', envVar: 'TILLY_SECRET_KEY', scopes: ['*'], }, ], });

Customer Roles

customerRoles declares the customer RBAC roles whose permissions become a customer access token’s scope set at mint time. Reconciled by foir push (matched by key within the project — declared keys are created or updated in place; roles present on the platform but absent from the config are left untouched, never auto-disabled).

Since the two-gate auth change (Model B), a consumer app’s public pk_ key can no longer grant write or execute scopes — the only source of those scopes is the customer’s role. Without a role, every customer holds a read-only token. Declare a single isDefault: true role to grant the baseline write/execute scopes every customer needs:

interface ApplyConfigCustomerRoleInput { /** Stable key, unique per project (e.g. "default"). */ key: string; /** Display name (e.g. "Default Customer"). */ name: string; /** Scopes granted to customers holding this role. */ permissions: string[]; /** When true, every customer inherits this role with no per-customer assignment. */ isDefault?: boolean; }
export default defineConfig({ key: 'tilly', name: 'Tilly', customerRoles: [ { key: 'default', name: 'Default Customer', // self:* expands at mint time to records:{read,write}:<model> for every // customer-writable model in the project. It does NOT include // operations:execute — list that explicitly when customers run operations. permissions: ['self:*', 'operations:execute'], isDefault: true, }, ], });

self:* depends on field-level access. A model only counts as “customer-writable” — and thus joins the self:* expansion — when one of its fields grants write to the self principal. If your models have no such field access configured, grant explicit records:read:<model> / records:write:<model> scopes instead.

Existing customers must log in again. Token refresh re-stamps the old scopes; only a fresh login picks up a newly added or changed role.

The same operations are available imperatively via the foir customer-roles CLI command (list, create, update, disable, assign, revoke, assignments).

Apps

Per-project app installations live under the top-level apps key. Each entry maps a manifest URL to the project’s models and any per-project settings. See Defining Apps for the full reference.

interface AppInput { source: string; // https URL to the manifest JSON settings?: Record<string, unknown>; mappings?: { sources?: Record<string, AppSourceMappingInput>; sinks?: Record<string, AppSinkMappingInput>; placementFields?: Record<string, AppPlacementFieldChoiceInput>; }; } interface AppSourceMappingInput { toModel: string; naturalKey: string; fields: Record<string, string>; // app-field-name → model-field-key } interface AppSinkMappingInput { toModel: string; naturalKey: string; fields: Record<string, string>; } interface AppPlacementFieldChoiceInput { model: string; field: string; }

Project Settings

The top-level project key pins the config to a project (guarding against pushing one folder’s config into another) and carries declarative project-level settings. The pin (id / tenantId) is reconciled by foir push; foir pull populates the settings block from live project state. Omit settings to leave the platform settings untouched.

interface ApplyConfigProjectInput { id: string; // Project id this folder is bound to tenantId: string; // Tenant id of the bound project name?: string; // Human-friendly name for diff readability settings?: ApplyConfigProjectSettingsInput; } interface ApplyConfigProjectSettingsInput { displayName?: string; // Name in customer-facing emails / portal logoUrl?: string; // Public logo URL (customer emails) primaryColor?: string; // Accent hex in customer emails fromName?: string; // "From" name on customer emails replyTo?: string; // "Reply-To" address supportEmail?: string; // Support email in email footers appBaseUrl?: string; // Public base URL; customer-email links built from this customerWelcomeEmailEnabled?: boolean; // Welcome email on registration (default true) customerSignupEnabled?: boolean; // Allow public CustomerRegister (default true) customerPasswordEnabled?: boolean; // Email/password customer login (default true) customerOtpEnabled?: boolean; // Email one-time-code login (default false) }

Relying Parties

relyingParties registers Login-with-Foir storefronts / external apps. See defineRelyingParty above for the full shape and example.

Design Tokens

designTokens carries a W3C Design Tokens document for the project, applied verbatim by foir push. See Design Tokens and defineDesignTokens.

Secrets

Secrets are not part of foir.config.ts. They are declared in a foir.secrets.ts file with defineSecrets and reconciled with foir secrets push.

Next Steps

Last updated on