Skip to Content

Apps

Apps are pluggable, installable units that extend a Foir project with credentials, operations, lifecycle hooks, custom UI, and inbound or outbound data flows. They’re the single concept the platform uses for every third-party or first-party extension — Shopify, Stripe, redirects, translation, image transforms, custom dashboards, and anything else you’d plug in.

Overview

An app is described by a single JSON manifest at a URL the developer chooses. The manifest declares what the app contributes; installing an app means pasting that URL into the admin (or referencing it from foir.config.ts), reviewing what will be added to your project, and mapping any source or sink types the app declares onto your project’s own models.

Paste manifest URL → Review install preview → Map sources/sinks to your models → Install

After install you authorize credentials (OAuth flow or an API-key form), and the app is live: its operations are callable, its hooks fire on the lifecycle events it subscribed to, and any UI it contributes appears in the admin.

What an app can contribute

CapabilityWhat it means
CredentialsA credential slot the app needs (OAuth, API key, SSH key, shared secret, or none).
OperationsNamed operations the platform calls to run the app’s logic.
HooksLifecycle subscriptions that fire app operations when records change.
UI placementsIframes embedded in the admin: full-page dashboards, main-editor tabs, sidebars, or field-level editors.
Inbound webhooksAn endpoint that receives webhooks from external services (Shopify, Stripe, GitHub).
Source typesInbound data shapes the app can deliver. Mapped onto a project model at install.
Sink contractsOutbound data shapes the app needs. Mapped onto a project model at install.

Apps never own models. Models always live in your project’s foir.config.ts. Apps describe abstract shapes they produce or consume; mapping each shape onto a concrete project model is your decision at install time.

Examples

AppWhat it does
ShopifyReceives product webhooks, transforms via your mapping, ingests into your product model.
Cloudflare RedirectorWatches your redirect model and pushes records out to Cloudflare KV.
DeepL TranslatorSidebar panel that translates selected fields via DeepL.
AlgoliaIndexes records on lifecycle events; provides a full-page admin dashboard.
CSV importerUpload UI inside the admin; an import operation parses and ingests.
Color pickerStatic iframe replacing the default input on color-type fields — no credentials, no backend.

The color picker is the friction floor: a manifest at any URL, an HTML file at any URL. Install it by pasting one URL.

How apps differ from project-written extensions

Both apps and project-written code use the same platform primitives — operations, hooks, schedules, capabilities. The difference is lifecycle and ownership:

Project-writtenApp
Where it’s declaredfoir.config.ts operations / hooks arraysApp manifest at a URL
When it gets pushedfoir push reads your configAdmin clicks Install; CLI reads apps.<name> block of your config
Who owns the URLYour project’s repoThe app developer’s deployment
Update mechanismEdit code, foir pushfoir apps update <name> (or admin Update button)
ReusabilityOne projectSame manifest installs into many projects

Use project-written operations for one-off project logic. Build an app when something will be reused across projects, and ship it with a manifest. See Building an App.

The manifest

When you install an app, you provide a URL pointing at its manifest. The manifest is a JSON document the app developer authored; you’ll typically read its contents from the install preview rather than fetch it directly. Required fields are name and version; everything else is optional.

{ "name": "redirector", "version": "1.2.0", "metadata": { "displayName": "Cloudflare Redirector", "description": "Sync redirect records to a Cloudflare Worker + KV.", "icon": "redirect", "author": "Bob's Country Bunker" }, "credentials": { "strategy": "API_KEY", "schema": { "type": "object", "properties": { "cloudflareApiToken": { "type": "string" }, "cloudflareAccountId": { "type": "string" } }, "required": ["cloudflareApiToken", "cloudflareAccountId"] } }, "settings_schema": { "type": "object", "properties": { "cloudflareZone": { "type": "string" }, "workerName": { "type": "string" } } }, "sinks": { "redirect": { "label": "Redirect", "natural_key": "source", "fields": { "source": { "type": "text", "semantic": "source-url", "required": true }, "target": { "type": "text", "semantic": "target-url", "required": true }, "status": { "type": "select", "semantic": "status-code", "required": true }, "priority": { "type": "number", "semantic": "priority", "required": false } } } }, "operations": [ { "key": "redirect-sync", "name": "Sync Redirects to Edge", "endpoint": "/operations/redirect-sync", "capabilities": ["credentials:read", "records:read:$redirect", "status:write"] } ], "hooks": [ { "key": "sync-on-create", "event": "RECORD_CREATED", "filter": { "sinkContract": "redirect" }, "operation": "redirect-sync" } ] }

Things to notice as a project admin reading a manifest:

  • Placeholders like $redirect reference the redirect sink contract from the same manifest. They’re resolved to your actual project model key when you map redirect to a model during install.
  • Capabilities carry model placeholders too: records:read:$redirect becomes records:read:redirect-rules if you map the sink to a redirect-rules model. The capability list tells you exactly what the app can read or write — the install preview surfaces this.
  • Reserved operation keys start with __. __uninstall runs when you remove the app (so the app can clean up external resources); __validate_credentials runs when you submit credentials so bad values are rejected before they’re saved.
  • run_after_install: true means an operation runs once automatically after install — typically used for one-time setup like provisioning a Cloudflare Worker.

For the project-side declaration shape (what goes in foir.config.ts), see Defining Apps.

Installing an app

Install is a two-step flow. The same flow is used by the admin UI and by the CLI; the admin renders the preview between steps, the CLI submits both back-to-back from foir.config.ts.

From the admin

  1. Go to Apps in the admin
  2. Paste the manifest URL (or pick from the catalog)
  3. Review the install preview:
    • Operations that will be added
    • Hooks that will be added
    • UI placements that will appear
    • Capabilities each operation will carry
    • Source types and sink contracts that need mapping
  4. Map source/sink types onto your project’s models — pick a target model, pick the natural-key field, and map each app-side field name to a project field key
  5. (For TARGET_FIELD placements with deferred field choice) pick the field
  6. Click Install
  7. Authorize credentials — see Authorizing credentials below

Either everything installs or nothing does. There’s no partially-installed state.

From the CLI

Declare the app in foir.config.ts:

apps: { redirector: { source: 'https://redirector.foir.dev/manifest.json', settings: { cloudflareZone: 'bobscountrybunker.com', workerName: 'foir-redirects-bcb' }, mappings: { sinks: { redirect: { toModel: 'redirect', naturalKey: 'sourcePattern', fields: { source: 'sourcePattern', target: 'targetPattern', status: 'statusCode', priority: 'priority', }, }, }, }, }, },

Then foir push. The CLI fetches the manifest, validates your mappings, and installs.

For ad-hoc installs without writing config, use foir apps install with inline mapping flags — see CLI Commands.

Authorizing credentials

After install, credentials still need to be written. They’re never carried in foir.config.ts — secrets stay out of repos.

OAuth apps (Shopify, Stripe, Figma, Google)

  1. From the app’s detail page, click Authorize
  2. The platform redirects you through the app’s OAuth flow
  3. After consent, the app records its credentials and you’re returned to the admin

API-key apps (Redirector, DeepL, Algolia)

  1. From the app’s detail page, click Configure credentials
  2. Fill the form (the fields are driven by the manifest’s credentials.schema)
  3. Click Save

If the app declares __validate_credentials, the platform tests your credentials before persisting them. Bad credentials are rejected at submit time with the error returned by the app — you never end up with a saved-but-broken credential slot.

Updating an app

Apps update via an explicit flow. The platform never silently re-fetches manifests in the background.

From the admin

  1. From the app’s detail page, click Check for updates
  2. Review the diff. Each change is classified:
    • Safe changes apply automatically when you click Apply update
    • Requires confirmation changes need explicit approval (e.g. an operation’s capabilities changed, a hook was removed)
    • Rejected changes block the update entirely (e.g. a mapped sink was removed; you’d need to clear the mapping first)
  3. Click Apply update

From the CLI

# Show the diff without applying foir apps update redirector --dry-run # Apply foir apps update redirector

A diff with any rejected changes blocks the update. Resolve those changes (e.g. clear a mapping) before update can proceed.

What changes how

ChangeClass
Added operation / hook / placementSafe
Added source type / sink contract (you’ll be prompted to map after)Safe
Changed metadata (description, icon, name)Safe
Changed operation timeout / retry policySafe
Removed operation that has never runSafe
Removed operation with execution historyRequires confirmation
Changed an operation’s endpoint or capabilitiesRequires confirmation
Changed credential strategy (you’ll need to reauthorize)Requires confirmation
Removed a hook or placementRequires confirmation
Changed a source-type field schema (type change)Rejected
Removed a source / sink that’s currently mappedRejected
Changed the manifest’s nameRejected

Uninstalling an app

From the admin

  1. From the app’s detail page, click Uninstall
  2. If the app declared __uninstall, the platform calls it first so the app can tear down external resources (Cloudflare Workers, Algolia indices, registered webhooks)
  3. The platform removes everything the app added: operations, hooks, placements, the credential slot, the inbound webhook route
  4. Your models stay — the project owns them

If the app’s service is unreachable, Force Uninstall removes the platform-side state without calling __uninstall. External resources may be orphaned; the warning is recorded in the app’s event log.

From the CLI

foir apps uninstall redirector foir apps uninstall redirector --force # skip __uninstall, remove anyway

Capabilities

Each operation an app contributes declares the capabilities its calls can use. Capabilities are surfaced in the install preview and on the app’s detail page so you always know what an app can read or write.

CapabilityWhat it grants the app
config:readRead the project’s config
credentials:readRead this app’s stored credentials
credentials:writeWrite this app’s stored credentials (OAuth flow)
status:writeUpdate the app’s status (last sync time, errors)
records:read:<model>Read records of a specific model
records:write:<model>Create / update records of a specific model
files:readRead files from project storage
files:writeUpload files

Apps must always declare a specific model on records:read and records:write. Bare records:read (no model suffix) is rejected — operations only see the models they explicitly listed.

Managing installed apps

The app’s detail page in the admin shows:

  • Current status (installed, authorized, last sync timestamp, error count)
  • Mappings (editable any time)
  • Settings (editable)
  • Operations contributed, with Run buttons for manual triggers
  • Hooks contributed, with enable/disable toggles
  • UI placements contributed
  • Recent events: install, update, errors, credential rotations
  • Update button when a new manifest version is available
  • Uninstall button

Via the CLI

# List installed apps foir apps list # Get an installed app foir apps get <name> # Install from a manifest URL (uses inline flags for ad-hoc; foir.config.ts is the golden path) foir apps install <manifestUrl> # Validate a manifest without installing foir apps validate <manifestUrl> # Trigger an operation contributed by an app foir apps trigger <appName> <operationKey> --data '{"key":"value"}' # Check for updates foir apps update <name> --dry-run # Apply available updates foir apps update <name> # Uninstall (calls __uninstall first if declared) foir apps uninstall <name> # Force-uninstall (skip the __uninstall call) foir apps uninstall <name> --force

The golden path for project-owned mappings is foir push — declare apps in foir.config.ts under apps.<name> and let the CLI reconcile. Inline --source-map / --sink-map flags on foir apps install are for ad-hoc admin work.

See Defining Apps and Push and Remove.

Building your own app

Apps are simple to ship: host a manifest JSON at any URL, deploy your service (if the app needs one), and share the URL. No package publish, no platform registry submission, no SDK lock-in. See Building an App for an end-to-end walkthrough.

Best practices

  • Declare every operation’s capabilities precisely. The fewer capabilities an operation needs, the smaller its blast radius.
  • Use __validate_credentials for any app where bad credentials would silently fail later — the validate hook surfaces the error at submit time, not on the first real call.
  • Use run_after_install: true for one-time setup operations (deploy a worker, create indices). You don’t have to remember to click Run after install.
  • Use sinks for outbound, sources for inbound. Don’t model “sync to Cloudflare” as a source — the project owns the data, the app pushes it out.
  • Apps don’t auto-update. The platform never re-fetches a manifest in the background. Update is always explicit, so an installed app keeps working even if the developer changes the URL or breaks the manifest.
  • Keep manifests small. If something doesn’t apply (no credentials, no middleware, no inbound webhook), omit it. The color picker’s manifest is six lines.

Next Steps

Last updated on