Skip to content

System Connections

A SystemConnection represents a link between a Tenant and an external system account. It holds the credentials and configuration required for Conflux to communicate with that account on the tenant's behalf. See Tenant Model for how connections, tenants, and adapters relate.

Model attributes

Attribute Type Notes
id UUID Primary key via HasUuids
tenant_id UUID (FK), nullable Owning Tenant; FK uses restrictOnDelete
system_id UUID (FK), nullable Target System; null while pending and not yet supplied by the plugin
name string Human-readable label for this system connection
external_user_id string, nullable User identifier on the external system
base_url string, nullable Root URL of the external system installation; null while pending and not yet supplied by the plugin
credentials array, nullable Sensitive secrets; stored as encrypted:json, never exposed by the API
status SystemConnectionStatus Enum: active, inactive, error, pending, disconnected, uninstalled
meta array Arbitrary metadata; cast to array
configuration array, nullable Connector-widget settings, including the data_types opt-in; cast to array. See Connection Config Fields
last_connected_at datetime Timestamp of the last successful connection
disconnected_at datetime, nullable Set for exactly as long as status is disconnected; the grace-period deadline is derived from it
deleted_at timestamp Soft deletes enabled

system_id and base_url can both be null simultaneously — this happens for a tenant-initiated connection that has not yet been finalized by the plugin (see Connection Flow). The API resource returns "system": null for these until a plugin supplies system_slug via /register.

SystemConnectionStatus enum

App\Enums\SystemConnectionStatus — backed string enum.

Case Value Meaning
Active "active" System connection is healthy and in use
Pending "pending" Awaiting first successful contact
Error "error" Last connection attempt failed
Inactive "inactive" Retired by a reconnect, or deactivated before the end states existed. Promises neither retention nor deletion
Disconnected "disconnected" Ended by the shop. Data and credentials are kept for the grace period but no longer served; reversible by reconnecting
Uninstalled "uninstalled" Ended by removing the plugin. Everything is deleted at once, including the connection row itself, so the state is short-lived

Note

Activating a connection — creating it Active, or changing its status to Active — automatically derives the tenant's default SyncRules. See Sync Rules — Automatic rule generation.

Grace period

disconnected_at is an invariant of the state rather than something a caller sets: SystemConnectionObserver::saving() stamps it when a connection enters disconnected without a timestamp, keeps a timestamp that was supplied, and clears it when the state is left. A reconnect therefore clears the deadline by itself.

$connection->graceDeadline();       // ?CarbonImmutable, null unless disconnected
$connection->isWithinGracePeriod(); // bool

The deadline is derived, never stored. The window defaults to 30 days and is configurable via config/conflux.php (disconnected_connection_grace_period_days, env DISCONNECTED_CONNECTION_GRACE_PERIOD_DAYS). See ADR-0013 for the model and the deletion rules that follow from it.

Relations and derived state

activeSourceSyncRules()

HasMany SyncRule keyed on source_connection_id, with is_active = true baked into the relation so no call site can forget it. Eager-load it with with('activeSourceSyncRules') whenever pullInCapabilities() is called for more than one connection.

pullInCapabilities()

$connection->pullInCapabilities(); // list<SystemCapability>

The data domains this connection may pull in right now: the capabilities its System declares, narrowed by configuredCapabilities() when the connection carries a connector-widget opt-in, and narrowed again to the data_domains of its active outgoing SyncRules. Returns [] when no System is linked. The result is ordered like System::capabilities, not like the rule rows.

Because it reads activeSourceSyncRules only, it answers the pull-in question exclusively — hence the name. It is not the answer for push-out, which does not exist yet and will need its own reader over incoming rules.

This is the single source for both dispatch paths — sync:dispatch and the panel's Bulk Sync action. See Sync Rules — Who reads the rules.

configuredCapabilities()

$connection->configuredCapabilities(); // list<SystemCapability>|null

The connector-widget opt-in from configuration.data_types, mapped through SyncDataType::toCapability(). null means the connection carries no opt-in and is therefore unrestricted; [] means the customer has selected nothing.

Scopes

scopeForTenant

SystemConnection::query()->forTenant($tenant)->get();

Filters to system connections owned by the given Tenant instance or tenant UUID. Applied by the list endpoint against the authenticated connection's tenant, so a caller only ever sees its own tenant's connections.

scopeForSystem

SystemConnection::query()->forSystem($system)->get();

Filters to system connections targeting the given System instance or system UUID.

scopeActive

SystemConnection::query()->active()->get();

Filters to rows where status = active.

scopeAccessibleBy

SystemConnection::query()->accessibleBy($user)->get();

Filters to connections whose tenant the given admin User may reach — a SuperAdmin short-circuits to everything, anyone else goes through Tenant::scopeAccessibleBy. This is the status-only set: it includes a platform-support user's sibling connections, which the admin panel surfaces read-only.

scopeFullyAccessibleBy

SystemConnection::query()->fullyAccessibleBy($user)->get();

Filters to connections whose System the user actually supports — the system must satisfy canHaveSupportUsers() and list the user in system_user. SuperAdmin short-circuits again.

This scope carries no tenant restriction of its own. It is used as a per-record predicate on rows that already passed accessibleBy (the admin resource applies that in getEloquentQuery()), which together yield "own systems under accessible tenants" — the set a support user may see in full, as opposed to the status-only siblings. See ADR-0007 for why access splits into two tiers, and Admin access model for the concept.

API endpoint

See the auto-generated API docs for the full endpoint specification, query parameters, and response shape.

Cleanup

A SystemConnection left in pending status with no way to ever complete its handshake (for example, repeated tenant-initiated /connect calls that are never finished by a plugin — see Connection Flow) is abandoned and safe to remove. The abandoned-connections:prune Artisan command hard-deletes every SystemConnection where:

  • status = pending,
  • created_at is older than a configurable grace period, and
  • there is no valid (non-expired, unused) SystemConnectionToken still associated with it.

Connections in any other status (active, inactive, error, disconnected, uninstalled) are never touched, regardless of age.

The grace period defaults to 24 hours and is configurable via config/conflux.php (pending_connection_grace_period_hours, env PENDING_CONNECTION_GRACE_PERIOD_HOURS).

Because system_connection_tokens.system_connection_id uses restrictOnDelete, the command first hard-deletes the connection's own (already-invalid) tokens, then hard-deletes the connections themselves — bypassing SoftDeletes on SystemConnection, the same precedent set by connection-tokens:prune (see Pruning).

It is scheduled to run daily (registered in routes/console.php) and can also be run manually:

docker compose exec php php artisan abandoned-connections:prune

Source files

File Purpose
app/Models/SystemConnection.php Eloquent model, casts, scopeActive, scopeForSystem, scopeForTenant, scopeAccessibleBy, scopeFullyAccessibleBy, activeSourceSyncRules(), pullInCapabilities()
app/Enums/SystemConnectionStatus.php Status backed enum
app/Http/Controllers/Api/SystemConnectionsController.php GET /api/system-connections invokable controller
app/Http/Controllers/Api/RegisterSystemConnectionController.php POST /api/system-connections/register — plugin and tenant registration flows
app/Http/Controllers/Api/ConnectSystemConnectionController.php POST /api/system-connections/connect — tenant-side initiation and plugin-side finalization
app/Http/Requests/ListSystemConnectionsRequest.php Request validation for the list endpoint
app/Http/Requests/RegisterSystemConnectionRequest.php Request validation for the registration endpoint
app/Http/Requests/ConnectSystemConnectionRequest.php Request validation for the connect endpoint
app/Http/Resources/SystemConnectionResource.php API resource transformer
app/Console/Commands/PruneAbandonedConnections.php abandoned-connections:prune command — deletes abandoned pending connections
routes/api.php Route definitions (api.system-connections, api.system-connections.register)
routes/console.php Daily schedule entry for the prune command
tests/Feature/Api/SystemConnectionsTest.php Feature tests

See also

  • Connection Flow — how plugin-initiated and tenant-initiated registration work and why the endpoint is unauthenticated
  • API Authentication — the connection guard, bearer token setup, EnsureSystemConnectionIsActive middleware, test helpers
  • Systems — system slugs used in the system filter
  • System Connection Tokens — short-lived tokens used to finalize a system connection, and its own connection-tokens:prune command
  • System Connections admin resource — Filament admin panel view of the same model