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 |
SystemConnectionState |
State: active, inactive, error, pending, disconnected, uninstalled. Changes only through a transition, see Status |
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 |
source_timezone |
string, nullable | The external system's own timezone, where the platform's datetimes need one. Deliberately absent from the #[Fillable] attribute: an integration writes it directly, never by mass assignment. null means not established, never a default, so an instant parsed against it is a recorded failure. See ADR-0018 |
source_locale |
string, nullable | The external system's own locale (de_DE), where a target needs a region a record does not carry. Its first consumer is outbound phone-number normalisation: a number in national notation resolves to one international number only against a region. A column of its own rather than a meta key, for the same reason as source_timezone — meta is hand-editable in the admin panel and a sync would overwrite it. Written by each integration's resolveSourceLocale() during verification: WooCommerce reads environment.language from the system status report, PrestaShop the default language's locale (or language_code on 1.6). Best effort — a shop that cannot answer keeps the stored value and still verifies. null costs a region rather than producing a wrong one |
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.
Status¶
status is a spatie/laravel-model-states state.
The abstract base is App\States\SystemConnection\SystemConnectionState, and each concrete state is
a final class next to it. Each state stores its $name alias in the column, so the stored values are
plain strings, never class names. See
ADR-0039 for why.
| State | Stored value | Badge colour | Meaning |
|---|---|---|---|
Active |
"active" |
success |
System connection is healthy and in use |
Pending |
"pending" |
warning |
Awaiting first successful contact. The default for a new row |
Error |
"error" |
danger |
The circuit breaker opened during a sync. Left through Reactivation |
Inactive |
"inactive" |
gray |
Deactivated via the admin panel, retired by a sibling reconnect, or deactivated before the Disconnected/Uninstalled end states existed. Promises neither retention nor deletion: tokens are revoked, business data is soft-deleted, credentials are kept |
Disconnected |
"disconnected" |
info |
Ended by the shop. Data and credentials are kept for the grace period but no longer served; reversible by reconnecting |
Uninstalled |
"uninstalled" |
danger |
Ended by removing the plugin. Everything is deleted at once, including the connection row itself, so the state is short-lived |
The label is the capitalised stored value. SystemConnectionState::options() returns every stored
value mapped to its label, for select filters.
| Method | Returns true for |
|---|---|
allowsDataFlow() |
Active only. SystemConnection::isHealthy() delegates to it |
hasGracePeriod() |
Disconnected only |
Transitions¶
A status changes only through $connection->status->transitionTo(Target::class). The library resolves
the transition class from the current and the target state. Every transition class lives in
App\States\SystemConnection\Transitions, and the table is declared in
SystemConnectionState::config().
| Transition | From | To | Side effects |
|---|---|---|---|
Activate |
Pending | Active | Resets the circuit breaker, generates default sync rules, stamps last_connected_at |
Reconnect |
Disconnected | Active | As Activate, plus RestoreConnectionDataJob with the previous disconnected_at |
Reactivate |
Error | Active | Resets the circuit breaker, generates missing default sync rules |
Deactivate |
Pending, Active, Error | Inactive | Cancels in-progress batches, revokes all tokens, NotifyConnectionDeactivatedJob, CleanupConnectionDataJob |
Disconnect |
Pending, Active, Error | Disconnected | Cancels in-progress batches, DisconnectConnectionDataJob |
RecordError |
Active | Error | Cancels in-progress batches |
Uninstall |
Pending, Active, Inactive, Disconnected, Error | Uninstalled | Cancels in-progress batches, revokes all tokens, clears credentials, PurgeConnectionJob |
Reconnect::canTransition() allows the move only while the connection is within its grace period;
past it, transitionTo(Active::class) throws Spatie\ModelStates\Exceptions\TransitionNotAllowed.
Every other move is refused. In particular, nothing leaves Uninstalled, and Inactive has no way back
to Active other than a fresh handshake, which creates a new row.
Every transition extends SystemConnectionTransition. Its final handle() does the following, in one
transaction:
- Throws a
LogicExceptionif the model has unsaved changes. Save other fields first, then transition. - Reloads the row into the same instance with
refreshForUpdate(), which selects it withlockForUpdate(). - Returns without saving if the stored state already is the target.
- Calls
refused()if the stored state may not move to the target. It also callsrefused()when the table resolves the stored move to a different transition class than the one running. By default that throwsApp\Exceptions\StaleConnectionStatus, aSpatie\ModelStates\Exceptions\CouldNotPerformTransition.RecordErroroverrides it to log atinfoand keep the stored state, because a connection that ended while a sync was probing the shop is an expected race. - Saves the target state and runs the side effects.
Synchronous side effects roll back together with the status. Jobs are dispatched with
->afterCommit(), so they wait for the outermost transaction to commit and are discarded on rollback.
The table has no ignoreSameState(). A transitionTo() into the state the instance already holds
throws CouldNotPerformTransition before the transition runs. A caller that can repeat a move returns
early when $connection->status instanceof Target.
Creation is not a transition. A new row defaults to Pending, and a row may be created in any state:
the tenant bootstrap and SyncRuleSeeder create Active rows, and factories create any state.
SystemConnectionObserver::saving() throws App\Exceptions\StatusChangeRequiresTransition when an
existing row's status changes outside a transition. A transition lifts the guard only for its own
save, through SystemConnection::allowingStatusChange(), and an architecture test keeps the base
transition its only caller. Query-builder updates, saveQuietly() and withoutEvents() bypass that
guard, so never use them to write the column.
To query by status, use whereState('status', Active::class). To compare, use
$connection->status instanceof Active. $connection->status->getValue() returns the stored string.
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.
Note
A store reconnecting under the same tenant_id + system_id + external_user_id triple retires its previous SystemConnection (sets it Inactive and clears external_user_id) rather than colliding with it — see SystemConnectionService::activate() below and Connection Flow — Deactivation and Reconnection. This is distinct from Disconnected and Uninstalled, which record a deliberate end to the connection rather than a retirement in favour of a new one — see Deactivation, Disconnect, and Uninstall below.
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¶
outgoingSyncRules() / incomingSyncRules()¶
HasMany SyncRule, keyed on source_connection_id and target_connection_id respectively — every rule that moves data out of, or into, this connection, active or not.
activeOutgoingSyncRules() / activeIncomingSyncRules()¶
Derived from the two relations above with is_active = true baked into the query, so no call site can forget it. Eager-load activeOutgoingSyncRules with with('activeOutgoingSyncRules') whenever pullInCapabilities() is called for more than one connection.
outgoingCoverageGap() / incomingCoverageGap()¶
$connection->outgoingCoverageGap(); // list<SystemCapability>
$connection->incomingCoverageGap(); // list<SystemCapability>
The data domains this connection could hand off (or receive) but no active rule in that direction covers, narrowed to domains for which a rule could actually be created: some eligibleCounterparts() connection accepts the domain in that direction per the direction matrix (ADR-0005). A domain no counterpart could ever route in that direction is left out — reporting it would make the warning permanent and unactionable. Read by the connection's flow overview in the admin panel; see System Connections admin resource.
eligibleCounterparts()¶
The active connections in this connection's tenant that a SyncRule may pair it with: exactly one side must be an internal consuming platform (ADR-0008), the same classification SyncRuleBlueprintService applies when deriving a tenant's default rules. Deliberately not narrowed by who is asking — outgoingCoverageGap() and incomingCoverageGap() read it as a statement about the connection itself, independent of the viewer.
pullInCapabilities()¶
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 activeOutgoingSyncRules only, it answers the pull-in question exclusively — hence the name. Push-out is a delivery layer CFX-206 is specifying, and no code delivers yet. activeIncomingSyncRules() still exists only to back incomingCoverageGap()'s reporting, rather than a delivery. See Outbound Delivery for the design.
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()¶
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¶
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¶
Filters to system connections targeting the given System instance or system UUID.
scopeActive¶
Filters to rows where status = active.
scopeAccessibleBy¶
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¶
Filters to a connection when either: its own System is one the user actually supports (System::scopeSupportedBy() — the system satisfies canHaveSupportUsers() and lists the user in system_user), or its system cannot have support users at all (System::scopeCannotHaveSupportUsers() — a shop adapter) and a sibling connection of the same tenant points at a system the user does support. 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, plus any shop connection reached through a sibling own system, under accessible tenants" — the set a support user may see in full, as opposed to the status-only siblings that remain (another internal/consuming platform's connection). See ADR-0007 for the original two-tier split, ADR-0008 for why shop adapters never have their own support members, and ADR-0030 for the sibling-cascade exception that follows from that. For the concept, see Admin access model.
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_atis older than a configurable grace period, and- there is no valid (non-expired, unused)
SystemConnectionTokenstill 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:
Deactivation¶
A connection moves to Inactive through the Deactivate transition, from Pending, Active or Error. Two callers use it: the DeactivateConnectionAction (see System Connections admin resource), and SystemConnectionService::activate() retiring a sibling connection on reconnect (see Connection Flow: Why Reconnection Must Retire the Old Connection). The edit form cannot set the status. The transition runs for any path that lands the connection on Inactive, not a single specific endpoint or action. It first cancels the connection's in-progress bus batches, then runs three independent clean-up steps:
- Tokens are revoked immediately and irrecoverably.
SystemConnectionService::revokeAllTokens()hard-deletes everySystemConnectionTokenbelonging to the connection — both handshake tokens and widget access tokens (see Connection Widget). This bypassesSoftDeletes— the same precedent asconnection-tokens:prune(see System Connection Tokens — Pruning). Once deactivated, a connection's old tokens cannot be restored; a subsequent reactivation does not reissue them. Credentials themselves (thecredentialscolumn) are not cleared — only tokens are revoked. - The plugin is notified, best-effort.
App\Jobs\NotifyConnectionDeactivatedJobis dispatched and, if the connection's integration implementsApp\Contracts\CanNotifyDeactivation, calls itsnotifyDeactivated()method so the plugin can update its own local state. Failures are logged, not retried. - Business data is soft-deleted asynchronously.
App\Jobs\CleanupConnectionDataJobis dispatched (queue:rabbitmq) and soft-deletes everyCustomer,Order,LineItem,Product, andCustomerConsentrow belonging to the connection, since the shop no longer authorises Conflux to hold or serve that data. This uses the same bulk soft-delete pattern described in Sync Batches, and, unlike token revocation, is recoverable at the database level — the rows are soft-deleted, not hard-deleted. Sync rules and batches are left untouched.
There is no grace period and no disconnected_at timestamp for a plain deactivation, and there is no admin action to move an Inactive connection back to Active (Reactivate applies only to Error, see Reactivation from Error). Reactivating requires completing a fresh handshake (see Connection Flow), which does not reissue the old tokens or restore the soft-deleted business data; it is repopulated by the next bulk or delta sync instead.
See Connection Flow — Deactivation and Reconnection for the wider narrative.
Reactivation from Error¶
A connection lands on Error through the RecordError transition, when the circuit breaker opens during BulkSyncJob or FetchBulkPageRangeJob. FetchBulkPageRangeJob also records Error when the connection's integration cannot export in bulk. If the connection left Active while the job was probing the shop, RecordError logs at info and keeps the stored state.
The circuit breaker closes after its cooldown, but the status stays Error. The way back is the admin panel's ReactivateConnectionAction, which runs the Reactivate transition from Error to Active:
- The circuit breaker is reset, so the next sync is let through.
- Missing default sync rules are generated, the same way an activation generates them.
Tokens, credentials and business data are untouched: Error removes none of them. Reactivation starts no sync itself. The next scheduled delta sync picks the connection up, and the Bulk Sync action starts one immediately.
Disconnect¶
POST /api/system-connections/disconnect is how the plugin reports, from its own admin UI, that the shop has disconnected. It is authenticated by the widget token, deliberately POST rather than DELETE (a business event, not a resource removal). The admin panel offers the equivalent DisconnectConnectionAction (see System Connections admin resource). Both set status = Disconnected on the connection and start a 30-day grace period, rather than ending the connection outright.
The Disconnect transition runs from Pending, Active or Error. It cancels in-progress bus batches and dispatches App\Jobs\DisconnectConnectionDataJob. The endpoint returns early when the connection already is Disconnected, so repeating the call is harmless and queues the job once. That job:
- Soft-deletes business data, exactly as deactivation's
CleanupConnectionDataJobdoes — everyCustomer,Order,LineItem,Product, andCustomerConsentrow belonging to the connection. - Soft-deletes the connection's
SyncRules too — unlike deactivation, which leaves sync rules untouched, so nothing tries to sync a connection that is no longer serving data during the grace period. Soft-delete rather thanis_active = false: that flag is an independent, admin-toggleable setting, and a rule an admin turned off before the disconnect must stay off on reconnect.
Credentials and tokens are kept, not revoked — a reconnect within the grace period needs no new credentials from the operator. disconnected_at is maintained as an invariant of the state by SystemConnectionObserver::maintainDisconnectedAt() (a saving() hook): stamped when the connection enters Disconnected without a timestamp, kept if one is already present, and cleared as soon as the status changes away from Disconnected. SystemConnection::graceDeadline() / isWithinGracePeriod() derive the deadline from it without ever persisting it separately — see Grace period above.
Reconnecting within the grace period. SystemConnectionService::activate() reactivates the existing Disconnected row itself, rather than activating a freshly created one, whenever a sibling with the same tenant_id/system_id/external_user_id is Disconnected. Moving it back to Active runs the Reconnect transition, which does what Activate does and additionally dispatches App\Jobs\RestoreConnectionDataJob with the disconnected_at the row held before the move. It restores only rows whose deleted_at is at or after disconnected_at (rows a reconcile run legitimately removed before the disconnect stay gone) and restores the connection's sync rules the same way, so a rule an admin had already turned off stays off.
Grace period expiry. SystemConnection implements Laravel's Prunable: prunable() selects Disconnected connections whose disconnected_at is older than the configured grace period, and pruning() — the same FK-safe cascade delete App\Jobs\PurgeConnectionJob uses for uninstall — hard-deletes everything belonging to the connection before prune() force-deletes the row itself. The daily model:prune --model=SystemConnection schedule entry (routes/console.php) runs this in chunks of 1000 with per-row fault isolation, so one connection failing to purge does not stop the rest. pruning() re-reads the connection's own state first and refuses (throwing ConnectionNotPurgeable) a connection that is Disconnected and still within its grace period, so a reconnect racing the batch is handled safely — the same guard PurgeConnectionJob relies on for the uninstall path, where it never fires because nothing transitions a connection away from Uninstalled.
Difference from Deactivation and Uninstall¶
| Deactivation | Disconnect (POST /disconnect) |
Uninstall (POST /uninstall) |
|
|---|---|---|---|
| Resulting status | Inactive |
Disconnected |
Uninstalled |
| Grace period | None | 30 days (configurable) | None |
| Token revocation | Immediate, irrecoverable | Kept | Immediate, irrecoverable |
| Credentials cleared | No | No | Yes (clearCredentials()) |
| Business data removal | Soft-delete | Soft-delete + sync rules deactivated | Hard-delete (PurgeConnectionJob) |
| Reversible? | Reconnect rebuilds data from next sync | Reconnect within window restores soft-deleted data (RestoreConnectionDataJob) and sync rules |
No |
See Connection Flow — Deactivation and Reconnection for the wider narrative and ADR-0013 for why disconnect and uninstall are modelled as distinct states.
Uninstall¶
POST /api/system-connections/uninstall is the plugin's signal that it has been physically removed from the merchant's store and Conflux must not retain any of the operator's personal data. It is authenticated by the widget token (the same credential POST /api/system-connections/disconnect uses) and deliberately uses POST rather than DELETE — it reports a business event, not a REST resource removal. The admin panel offers the equivalent UninstallConnectionAction.
The endpoint moves the caller's own connection to Uninstalled through the Uninstall transition and returns immediately. It returns early when the connection already is Uninstalled, so repeating the call is harmless. The transition runs from every state but Uninstalled. It cancels in-progress bus batches and runs two steps synchronously, before dispatching an asynchronous purge:
- Tokens are revoked and credentials are cleared immediately and irrecoverably, synchronously.
SystemConnectionService::revokeAllTokens()hard-deletes everySystemConnectionTokenfor the connection, andSystemConnectionService::clearCredentials()wipes thecredentialscolumn viaupdateQuietly. Both complete before the endpoint returns a response. - All personal data is hard-deleted asynchronously.
App\Jobs\PurgeConnectionJobis dispatched and permanently removes, usingwithTrashed()so already-soft-deleted rows are included too, everyLineItem/Order,CustomerConsent/Customer,Product,SyncBatch, andSyncRulebelonging to the connection, then itsSystemConnectionTokens andWebhookCalls, and finally theSystemConnectionrow itself — an FK-safe order, since every foreign key usesrestrictOnDeleteand there are no cascades.
PurgeConnectionJob is the single shared hard-delete mechanism for both uninstall and grace-period expiry (see Disconnect above): it re-checks the connection's status when it runs and only proceeds for Uninstalled connections or Disconnected ones past their grace deadline, so Uninstalled is intentionally short-lived — the connection row itself is gone once the job completes.
Warning
Uninstall is irreversible. Hard-deleted rows cannot be restored by a later reconnect — a new sync would need to repopulate them from scratch.
Difference from Deactivation and Disconnect¶
See Difference from Deactivation and Uninstall under Disconnect above for the full three-way comparison.
See Connection Flow — Uninstall for the wider narrative and ADR-0013 for why disconnect and uninstall are modelled as distinct states.
Source files¶
| File | Purpose |
|---|---|
app/Models/SystemConnection.php |
Eloquent model, casts, scopeActive, scopeForSystem, scopeForTenant, scopeAccessibleBy, scopeFullyAccessibleBy, outgoingSyncRules()/incomingSyncRules(), activeOutgoingSyncRules()/activeIncomingSyncRules(), outgoingCoverageGap()/incomingCoverageGap(), eligibleCounterparts(), pullInCapabilities() |
app/States/SystemConnection/SystemConnectionState.php |
Abstract status state: the transition table in config(), canTransitionToVia() (whether a move runs through exactly one transition class), labels, colours, allowsDataFlow(), hasGracePeriod(), options() |
app/States/SystemConnection/*.php |
The six concrete states, one per stored value |
app/States/SystemConnection/Transitions/SystemConnectionTransition.php |
Base transition: row lock, re-check against the stored state with canTransitionToVia(), save and side effects in one transaction |
app/States/SystemConnection/Transitions/Concerns/CancelsInProgressBatches.php |
cancelInProgressBatches() for the transitions that take a connection off Active: Deactivate, Disconnect, Uninstall, RecordError |
app/States/SystemConnection/Transitions/*.php |
Activate, Reconnect, Reactivate, Deactivate, Disconnect, RecordError, Uninstall, each with its side effects |
app/Exceptions/StatusChangeRequiresTransition.php |
Thrown by the observer's saving() guard when an existing row's status changes outside a transition |
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/Controllers/Api/DisconnectSystemConnectionController.php |
POST /api/system-connections/disconnect — plugin-initiated disconnect, authenticated by the widget token |
app/Http/Controllers/Api/UninstallSystemConnectionController.php |
POST /api/system-connections/uninstall — plugin-initiated uninstall, authenticated by the widget token |
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 |
App\Models\SystemConnection::prunable() / pruning() |
The Prunable deletion contract — selects Disconnected connections past their grace deadline, and the shared FK-safe cascade delete pruning() runs before prune() force-deletes the row; also the guard PurgeConnectionJob reuses for uninstall |
app/Observers/SystemConnectionObserver.php |
saving(): rejects a status change outside a transition, applies the default configuration, maintains disconnected_at; created()/restored(): sync rule generation for a row that is Active |
App\Services\SystemConnectionService::revokeAllTokens() |
Hard-deletes every SystemConnectionToken (handshake and widget) on deactivation or uninstall |
App\Services\SystemConnectionService::clearCredentials() |
Wipes the credentials column on uninstall |
app/Contracts/CanNotifyDeactivation.php |
Interface an integration implements to be notified of deactivation |
app/Jobs/NotifyConnectionDeactivatedJob.php |
Queued job — best-effort notification to the plugin on deactivation |
app/Jobs/CleanupConnectionDataJob.php |
Queued job — soft-deletes a deactivated connection's Customer/Order/LineItem/Product/CustomerConsent rows |
app/Jobs/DisconnectConnectionDataJob.php |
Queued job — soft-deletes business data and sync rules on disconnect |
app/Jobs/RestoreConnectionDataJob.php |
Queued job — restores soft-deleted business data and sync rules on reconnect within the grace period |
app/Jobs/PurgeConnectionJob.php |
Queued job — calls SystemConnection::prune() for an uninstalled connection |
app/Exceptions/ConnectionNotPurgeable.php |
Thrown by pruning() and caught by Prunable::pruneAll()/PurgeConnectionJob when a connection is Disconnected and still within its grace period |
app/Filament/Resources/SystemConnections/Actions/ReactivateConnectionAction.php |
Filament action: moves an Error connection to Active |
app/Filament/Resources/SystemConnections/Actions/DeactivateConnectionAction.php |
Filament action: moves the connection to Inactive |
app/Filament/Resources/SystemConnections/Actions/DisconnectConnectionAction.php |
Filament action: moves the connection to Disconnected |
app/Filament/Resources/SystemConnections/Actions/UninstallConnectionAction.php |
Filament action: moves the connection to Uninstalled |
routes/api.php |
Route definitions (api.system-connections, api.system-connections.register, api.system-connections.uninstall) |
routes/console.php |
Daily schedule entries for the prune commands |
tests/Feature/Api/SystemConnectionsTest.php |
Feature tests for registration, connection, and list endpoints |
tests/Feature/Api/DisconnectSystemConnectionTest.php |
Feature tests for the disconnect endpoint |
tests/Feature/Api/UninstallSystemConnectionTest.php |
Feature tests for the uninstall endpoint |
tests/Integration/Jobs/CleanupConnectionDataJobTest.php |
Integration tests for the deactivation soft-delete job |
tests/Integration/Jobs/DisconnectConnectionDataJobTest.php |
Integration tests for the disconnect soft-delete and sync-rule-deactivation job |
tests/Integration/Jobs/RestoreConnectionDataJobTest.php |
Integration tests for the reconnect restore job |
tests/Integration/Jobs/NotifyConnectionDeactivatedJobTest.php |
Integration tests for the deactivation notification job |
tests/Integration/Jobs/PurgeConnectionJobTest.php |
Integration tests for the uninstall purge job |
tests/Integration/Models/SystemConnectionTest.php |
Integration tests for prunable(), pruning()/prune(), and the model:prune wiring |
tests/Unit/States/SystemConnection/TransitionTableTest.php |
Every (from, to) pair against the transition table |
tests/Unit/States/SystemConnection/SystemConnectionStateTest.php |
Stored value, label, colour and helpers per state; the Pending default |
tests/Integration/States/SystemConnection/SystemConnectionTransitionTest.php |
The base transition: re-check against the stored row, same-state no-op, unsaved changes, lifting the status guard only for its own save, the lost RecordError race |
tests/Integration/States/SystemConnection/TransitionSideEffectsTest.php |
Side effects per transition, the disconnected_at invariant, that a save without a status change runs none of them, and rollback on a failing side effect |
See also¶
- Inactive-Connection Data-Flow Gating: how
isHealthy()is used at every data-flow entry point and how the transitions cancel in-progress batches on status change - Connection Flow — how plugin-initiated and tenant-initiated registration work and why the endpoint is unauthenticated
- API Authentication — the
connectionguard, bearer token setup,EnsureSystemConnectionIsActivemiddleware, test helpers - Systems — system slugs used in the
systemfilter - System Connection Tokens — short-lived tokens used to finalize a system connection, and its own
connection-tokens:prunecommand - System Connections admin resource: Filament admin panel view of the same model, including the Reactivate, Deactivate, Disconnect, and Uninstall actions
- Sync Batches — the bulk soft-delete pattern
CleanupConnectionDataJobfollows - ADR-0013 — why disconnect and uninstall are modelled as distinct states
- ADR-0039: why the status is a state machine with its own row lock and guard