Connector Architecture¶
Overview¶
Floh's connector system provides a unified interface for integrating with external services, whether SaaS platforms, on-premise applications, or custom APIs. Connectors abstract the specifics of each integration behind a standardized execution model, allowing workflows to interact with any system through a consistent set of commands.
Design Principles¶
- Multi-modal execution — Support built-in, scripted, OAS-derived, and external connectors through a single registry
- Secure by default — Sandboxed script execution, encrypted secrets, and scoped permissions
- Observable — Integrated logging with configurable levels, impact analysis, and audit trails
- Testable — Built-in mock mode at multiple granularity levels
- Evolvable — Schema versioning with breaking change detection
Execution Models¶
┌─────────────────────────────────────────────────────────────────┐
│ Connector Registry │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐│
│ │ Built-in │ │ Script │ │ OAS │ │ External ││
│ │ (Native) │ │ (QuickJS) │ │ (Generated)│ │ (HTTP) ││
│ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ └─────┬─────┘│
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────┐│
│ │ TypeScript │ │ Worker │ │ Worker │ │ HTTP ││
│ │ function │ │ Thread + │ │ Thread + │ │ POST ││
│ │ call │ │ QuickJS VM │ │ QuickJS VM │ │ /execute ││
│ └────────────┘ └────────────┘ └────────────┘ └───────────┘│
└─────────────────────────────────────────────────────────────────┘
Built-in (built_in)¶
Native TypeScript connectors compiled into the server. These have direct access to Node.js APIs and are registered in the connector registry at startup. Examples include the http, delay, google-workspace, and outbound scim connectors. The outbound SCIM connector is reference-grade for IdP provisioning use cases — see Outbound SCIM Connector.
Built-in types are authored with the defineConnector() framework under modules/connectors/define/ — a typed DSL that captures a connector's schema, commands, and handlers in one module and auto-derives:
- The runtime handler registered with
registerConnector()(viaauto-discover.tsat bootstrap). - The
ConnectorSeedemitted to theconnector_typetable (viaderive-seed.ts), preserving the legacyconfigSchemawire format.
Adding a new built-in type is a single module under handlers/ plus one entry in handlers/index.ts; routes.ts, registry.ts, and seed-connectors.ts require no edits. See the Creation Guide for worked examples.
Best for: Core integrations that ship with Floh, performance-critical connectors.
Script (script)¶
Custom JavaScript connectors executed in a sandboxed QuickJS environment via Node.js worker_threads. Scripts communicate with the host through a controlled floh.* API surface.
Best for: Custom integrations, user-authored connectors, connectors that need isolation.
OAS-Derived (oas)¶
Connectors auto-generated from OpenAPI 3.x specifications. The OAS parser extracts operations, parameters, and security schemes to produce commands and a generated script that runs in the same QuickJS sandbox as script connectors.
Best for: Rapid integration with APIs that provide OpenAPI specs.
External (external)¶
Remote connectors accessed over HTTP. Floh sends a POST request to the connector's /execute endpoint with the command, parameters, and metadata. External connectors implement a standardized protocol.
Best for: Connectors running in separate processes, microservices, or managed by third parties.
Data Flow¶
Workflow Step → StepExecutor → Mock Check → ConnectorRegistry
│ │
▼ ▼
ConnectorLogger Execution Model Router
│ ┌────┬────┬────┬────┐
▼ │BI │Scr │OAS │Ext │
LogService └────┴────┴────┴────┘
│ │
▼ ▼
system_log table ConnectorResult
- The StepExecutor receives a connector step from the workflow engine
- It looks up the connector definition from the database
- A ConnectorLogger is created with the connector's configured log level
- If mock mode is active (step, connector, run, or system level), mock execution runs instead
- Otherwise, the ConnectorRegistry routes execution based on
execution_model - The result is returned to the workflow engine as output variables
Database Schema¶
connector_definition table¶
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
name |
VARCHAR | Unique connector name |
type |
VARCHAR | Connector type (maps to registry handler) |
description |
TEXT | Human-readable description |
version |
VARCHAR | Semantic version |
execution_model |
VARCHAR | built_in, script, oas, external |
script_source |
TEXT | JavaScript source for script/OAS connectors |
mock_data |
JSONB | Static mock scenarios per command |
mock_script |
TEXT | JavaScript source for dynamic mocks |
mock_enabled |
BOOLEAN | Whether mock mode is active |
log_level |
VARCHAR | Minimum log level: trace/debug/info/warn/error/fatal |
debug_logging |
BOOLEAN | Override log level to debug when true |
oas_spec |
TEXT | Original OpenAPI spec (for reference) |
endpoint_url |
VARCHAR | External connector endpoint URL |
endpoint_auth |
JSONB | Authentication config for external endpoints |
schema_version |
VARCHAR | Schema version for breaking change tracking |
category |
VARCHAR | Organizational category (e.g., crm, hr, finance) |
tags |
JSONB | Array of tags for filtering |
icon |
VARCHAR | Icon identifier for UI display |
deprecated_at |
TIMESTAMP | When the connector was deprecated |
system_log table additions¶
| Column | Type | Description |
|---|---|---|
connector_id |
VARCHAR | Links log entries to a specific connector |
Security Model¶
Secret Encryption¶
Connection configuration containing secrets (API keys, tokens, passwords) is encrypted at rest using AES-256-GCM. Secret fields are identified via the configSchema — any field with secret: true is encrypted before storage and decrypted only at execution time.
Script Sandboxing¶
Script connectors run inside quickjs-emscripten VMs in isolated worker_threads:
- Memory limit: Configurable per connector (default 16 MB)
- CPU limit: Execution timeout (default 30s, configurable)
- No filesystem access: Scripts cannot read/write files
- No network access: All HTTP calls go through
floh.http.*proxied to the main thread - No module imports: Scripts must be self-contained
Built-in HTTP Connector URL Policy¶
The built-in http connector enforces outbound URL safety checks before sending
requests:
- Only
httpandhttpsURLs are allowed - Loopback and local names such as
localhostare blocked - Private/link-local IP ranges (RFC1918,
169.254.0.0/16,::1,fe80::/10,fc00::/7) are blocked - Known metadata hosts such as
metadata.google.internalare blocked
This policy prevents workflows from using interpolated variables to call internal network targets.
Permission Model¶
| Permission | Operations |
|---|---|
connector:read |
List, view, get registry, impact analysis |
connector:manage |
Create, update, delete, test, execute, parse OAS |
Mock Connector Guardrails¶
Built-in test connectors (test-ldap, test-db, test-activedirectory) are
kept available for workflow design and dry-run development in production
tenants, but are explicitly labeled and constrained:
/api/connectors/registrynow marks connector handlers withisMock.- Manual execution of mock connectors via
/api/connectors/:id/executeis restricted to users with theadminrole. - Manual executions are audit-tagged as
connector.mock_executed(real integrations useconnector.executed) so log and compliance queries can distinguish simulation traffic from real integration activity.
Resource Sync & User Matching¶
Connectors that declare sync-capable commands (e.g., listUsers) can synchronize external records into the connector_resource table. A post-sync matching step then links each resource to a Floh user account.
Match strategies¶
| Strategy | Key field(s) | DB lookup |
|---|---|---|
email |
resource.email |
v_user.email (case-insensitive) |
email_and_issuer |
resource.email + issuer from dot-path |
v_user.email + v_user.upstream_issuer (compound key) |
upstream_identity |
resource.externalId |
v_user.upstream_id |
external_id |
resource.externalId |
v_user.sub |
The email_and_issuer strategy resolves the issuer value from the synced resource using a configurable dot-path (issuerSourcePath, default attributes.identityIssuer). This is useful when matching against identity providers where the same email address may appear under different issuers.
When createUsers is enabled and users are auto-created during sync, the upstream_issuer and upstream_id fields are populated from the resource attributes so that future compound matching works correctly.
Versioning & Backwards Compatibility¶
Connector types follow semantic versioning (MAJOR.MINOR.PATCH). The system enforces version discipline at multiple levels.
Semver Enforcement¶
- Create: The
POST /api/connector-typesendpoint validates thatversionis a valid semver string. - Update: When
configSchemachanges onPUT /api/connector-types/:id, the system runscompareSchemas()to detect breaking/minor/patch changes and requires the caller to supply a version that satisfies the bump level — or setautoBump: trueto let the system compute the next version. - Seed time: Built-in connector seeds run
compareSchemas()at startup against the existing DB row. Breaking changes log a warning so operators notice schema drift between releases.
Schema Change Detection¶
The compareSchemas() function in schema-versioning.ts classifies changes as:
| Change | Classification |
|---|---|
| Command removed | Breaking (major) |
| Parameter removed | Breaking (major) |
| New required parameter (no default) | Breaking (major) |
| Required connectionConfig field added | Breaking (major) |
| Output removed | Breaking (major) |
| connectionConfig field removed | Breaking (major) |
| New command added | Minor |
| New parameter with default | Minor |
| Optional connectionConfig field added | Minor |
| New output added | Minor |
Use POST /api/connector-types/:id/compare-schema or POST /api/connectors/:id/compare-schema (instance-level, compares against the instance's type schema) to preview changes before applying them.
Command Deprecation¶
Commands can be marked deprecated in the configSchema:
{
"commands": {
"listUsers": {
"params": ["limit"],
"description": "List users",
"deprecated": true,
"deprecatedMessage": "Use listUsersV2 instead",
"addedInVersion": "1.0.0",
"removedInVersion": "3.0.0"
}
}
}
When a deprecated command is invoked at runtime, the system emits a structured warning log ("Deprecated command invoked") without failing the execution. This allows operators to track deprecated command usage and plan migrations.
Capabilities Endpoint¶
GET /api/connector-types/:id/capabilities and GET /api/connectors/:id/capabilities return the connector's command map with deprecation metadata and current version, enabling UI and automation to discover what a connector supports.
External Protocol Versioning¶
External connector /execute requests include a protocolVersion field (currently "1.0") so remote connectors can detect which request shape Floh is sending:
{
"protocolVersion": "1.0",
"command": "listUsers",
"config": { ... },
"params": { ... },
"variables": { ... },
"metadata": { "stepId": "...", "runId": "...", "timeout": 30000 }
}
Future protocol versions will be additive within a major version — fields may be added but never removed.
Migration Types¶
The shared ConnectorMigration type supports declarative upgrade metadata:
interface ConnectorMigration {
fromVersion: string;
toVersion: string;
description: string;
commandRenames?: Record<string, string>;
paramRenames?: Record<string, Record<string, string>>;
}
This enables future tooling to automate connector instance config and workflow step migrations when connector types are upgraded.
Module Structure¶
packages/server/src/modules/connectors/
├── registry.ts # Public execution dispatch + `executeConnector`
├── registry-internal.ts # Leaf module: in-memory built-in registry map
├── repository.ts # Database operations
├── routes.ts # API endpoints
├── connector-logger.ts # Structured logging via LogService
├── connector-debug.ts # Console debug logging (CONNECTOR_DEBUG env)
├── mock-engine.ts # Mock execution engine
├── schema-versioning.ts # Breaking change detection
├── impact-analysis.ts # Workflow impact scanner
├── oas-parser.ts # OpenAPI spec parser
├── script-runtime.ts # Worker thread management
├── script-worker.ts # QuickJS sandbox (runs in worker)
├── agent-protocol.ts # On-premise agent protocol
├── agent-routes.ts # WebSocket agent endpoints
├── rotate-keys.ts # Secret key rotation
├── http-url-policy.ts # Outbound URL safety checks (SSRF guard)
├── authifi-connector.ts # Built-in Authifi connector (legacy style)
├── authifi-client.ts # Authifi API client
├── test-connectors.ts # Built-in test connectors (legacy style)
├── define/ # `defineConnector()` framework
│ ├── define-connector.ts # `defineConnector()` / `defineCommand()` / `t.*`
│ ├── schema-builders.ts # `t.*` builders (string, secret, select, raw, …)
│ ├── types.ts # `ConnectorDefinition`, `InferShape<>`, etc.
│ ├── dispatcher.ts # `executeDefinition()` + `runInCodeMock()`
│ ├── derive-seed.ts # `ConnectorDefinition` → legacy `ConnectorSeed`
│ ├── register-definition.ts# Bridge to `registerConnector()`
│ └── auto-discover.ts # `loadBuiltInConnectors()`
├── clients/ # Shared client helpers for built-in handlers
│ └── http-connector-client.ts # SSRF-safe `connectorHttpRequest()`
└── handlers/ # Built-in connector modules
├── index.ts # BUILT_IN_DEFINITIONS + auto-registration
├── http.ts # http connector (defineConnector-based)
└── delay.ts # delay connector (defineConnector-based)
Authoring → registration → seeding flow¶
handlers/widget.ts (defineConnector({ name, commands }))
│
▼
handlers/index.ts (BUILT_IN_DEFINITIONS.push(widget))
│
├─▶ loadBuiltInConnectors() → registerDefinition() → registerConnector()
│ (available to `executeConnector("widget", …)` at runtime)
│
└─▶ seed-connectors.ts → toConnectorSeed(widget) → upsertConnectorType()
(creates `connector_type` row so UI can list + instantiate it)
Tests and the server boot path both import registry.ts, which side-effect-imports handlers/index.js — the registration invariant ("importing the registry gives you all built-ins") is preserved by the registry-internal.ts leaf module, which breaks what would otherwise be a cycle between registry.ts and handlers/*.