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 → InstallAfter 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
| Capability | What it means |
|---|---|
| Credentials | A credential slot the app needs (OAuth, API key, SSH key, shared secret, or none). |
| Operations | Named operations the platform calls to run the app’s logic. |
| Hooks | Lifecycle subscriptions that fire app operations when records change. |
| UI placements | Iframes embedded in the admin: full-page dashboards, main-editor tabs, sidebars, or field-level editors. |
| Inbound webhooks | An endpoint that receives webhooks from external services (Shopify, Stripe, GitHub). |
| Source types | Inbound data shapes the app can deliver. Mapped onto a project model at install. |
| Sink contracts | Outbound 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
| App | What it does |
|---|---|
| Shopify | Receives product webhooks, transforms via your mapping, ingests into your product model. |
| Cloudflare Redirector | Watches your redirect model and pushes records out to Cloudflare KV. |
| DeepL Translator | Sidebar panel that translates selected fields via DeepL. |
| Algolia | Indexes records on lifecycle events; provides a full-page admin dashboard. |
| CSV importer | Upload UI inside the admin; an import operation parses and ingests. |
| Color picker | Static 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-written | App | |
|---|---|---|
| Where it’s declared | foir.config.ts operations / hooks arrays | App manifest at a URL |
| When it gets pushed | foir push reads your config | Admin clicks Install; CLI reads apps.<name> block of your config |
| Who owns the URL | Your project’s repo | The app developer’s deployment |
| Update mechanism | Edit code, foir push | foir apps update <name> (or admin Update button) |
| Reusability | One project | Same 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
$redirectreference theredirectsink contract from the same manifest. They’re resolved to your actual project model key when you mapredirectto a model during install. - Capabilities carry model placeholders too:
records:read:$redirectbecomesrecords:read:redirect-rulesif you map the sink to aredirect-rulesmodel. The capability list tells you exactly what the app can read or write — the install preview surfaces this. - Reserved operation keys start with
__.__uninstallruns when you remove the app (so the app can clean up external resources);__validate_credentialsruns when you submit credentials so bad values are rejected before they’re saved. run_after_install: truemeans 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
- Go to Apps in the admin
- Paste the manifest URL (or pick from the catalog)
- 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
- 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
- (For
TARGET_FIELDplacements with deferred field choice) pick the field - Click Install
- 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)
- From the app’s detail page, click Authorize
- The platform redirects you through the app’s OAuth flow
- After consent, the app records its credentials and you’re returned to the admin
API-key apps (Redirector, DeepL, Algolia)
- From the app’s detail page, click Configure credentials
- Fill the form (the fields are driven by the manifest’s
credentials.schema) - 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
- From the app’s detail page, click Check for updates
- 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)
- Click Apply update
From the CLI
# Show the diff without applying
foir apps update redirector --dry-run
# Apply
foir apps update redirectorA diff with any rejected changes blocks the update. Resolve those changes (e.g. clear a mapping) before update can proceed.
What changes how
| Change | Class |
|---|---|
| Added operation / hook / placement | Safe |
| Added source type / sink contract (you’ll be prompted to map after) | Safe |
| Changed metadata (description, icon, name) | Safe |
| Changed operation timeout / retry policy | Safe |
| Removed operation that has never run | Safe |
| Removed operation with execution history | Requires confirmation |
| Changed an operation’s endpoint or capabilities | Requires confirmation |
| Changed credential strategy (you’ll need to reauthorize) | Requires confirmation |
| Removed a hook or placement | Requires confirmation |
| Changed a source-type field schema (type change) | Rejected |
| Removed a source / sink that’s currently mapped | Rejected |
Changed the manifest’s name | Rejected |
Uninstalling an app
From the admin
- From the app’s detail page, click Uninstall
- If the app declared
__uninstall, the platform calls it first so the app can tear down external resources (Cloudflare Workers, Algolia indices, registered webhooks) - The platform removes everything the app added: operations, hooks, placements, the credential slot, the inbound webhook route
- 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 anywayCapabilities
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.
| Capability | What it grants the app |
|---|---|
config:read | Read the project’s config |
credentials:read | Read this app’s stored credentials |
credentials:write | Write this app’s stored credentials (OAuth flow) |
status:write | Update 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:read | Read files from project storage |
files:write | Upload 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> --forceThe 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_credentialsfor 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: truefor 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
- Defining Apps — Project-side
foir.config.tsdeclaration - Building an App — Step-by-step guide to building and shipping an app
- Operations — Operation contract and execution modes
- Editor SDK — Build app UI placements