Peer-to-peer data sync module for PhoenixKit.
Provides bidirectional data synchronization between PhoenixKit instances — sync between dev and prod, dev and dev, or different websites entirely.
Add to your parent app's mix.exs:
{:phoenix_kit_sync, "~> 0.1"}The module is auto-discovered via PhoenixKit's beam scanning — no additional configuration needed. Enable it from the admin dashboard under Modules.
The module uses permanent connections with automatic cross-site registration:
- Sender creates a connection pointing to a remote site's URL
- System automatically notifies the remote site via API
- Remote site registers the connection based on their incoming settings:
- Auto Accept: Connection activates immediately
- Require Approval: Connection appears as pending
- Require Password: Only accepts with correct password
- Deny All: Rejects the connection request
- Both sites have matching connection records for data sync
- All transfers are tracked in the history with full audit trail
- Sender: "I allow this remote site to pull data from me"
- Receiver: "I can pull data from this remote site" (auto-created via API)
When you create a sender connection, the remote site automatically receives a corresponding receiver connection.
- Ephemeral Code-Based Transfers: One-time manual sync with 8-character secure codes
- Permanent Token-Based Connections: Recurring sync with full access controls
- Token-Based Authentication: Secure tokens, hashed in database, shown only once
- Approval Modes: Auto-approve, require approval, or per-table approval
- Access Controls: Allowed/excluded tables, download limits, record limits
- Security Features: IP whitelist, time-of-day restrictions, expiration dates
- Conflict Resolution: skip, overwrite, merge, append strategies
- Transfer Tracking: Full history with statistics and approval workflow
- Audit Trail: Track who created, approved, suspended, or revoked connections
- Real-Time Progress: Live tracking of sync operations
- Background Import: Async processing via Oban workers
- Cross-Site Protocol: HTTP API + WebSocket for data transfer
- Activity Audit: Every connection mutation (create, approve, suspend, revoke, reactivate, delete, token rotate) and every transfer mutation (create, complete, fail, cancel, approve, deny) writes a
sync.<resource>.<verb>row tophoenix_kit_activitieswith the actor's UUID — visible in the core admin Activity feed - Centralised error translation: Context functions return
{:error, :atom}tuples; UI / API layers translate viaPhoenixKitSync.Errors.message/1, which dispatches to gettext-wrapped strings (translations live in corephoenix_kit)
| Setting | Description |
|---|---|
| Approval Mode | auto_approve, require_approval, or per_table |
| Allowed Tables | Whitelist of tables the receiver can access |
| Excluded Tables | Blacklist of tables to hide from receiver |
| Auto-Approve Tables | Tables that don't need approval (when mode is per_table) |
| Max Downloads | Limit total number of transfer sessions |
| Max Records Total | Limit total records that can be downloaded |
| Max Records Per Request | Limit records per single request (default: 10,000) |
| Rate Limit | Requests per minute limit (default: 60) |
| Download Password | Optional password required for each transfer |
| IP Whitelist | Only allow connections from specific IPs |
| Allowed Hours | Time-of-day restrictions (e.g., only 2am-5am) |
| Expiration Date | Auto-expire the connection after a date |
| Status | Description |
|---|---|
| Pending | Just created, awaiting activation |
| Active | Ready to accept connections |
| Suspended | Temporarily disabled (can be reactivated) |
| Revoked | Permanently disabled |
| Expired | Auto-expired due to limits or date |
Control how your site handles connection requests from other sites:
| Mode | Behavior |
|---|---|
| Auto Accept | Incoming connections activate immediately |
| Require Approval | Connections appear as pending, need manual approval |
| Require Password | Sender must provide correct password |
| Deny All | Reject all incoming connection requests |
- Navigate to the Sync connections page in the admin dashboard
- Click "New Connection"
- Enter a name and the remote site's URL
- Configure access controls (approval mode, tables, limits)
- Save — the connection is created and token generated
- The remote site is notified automatically!
- If successful, the connection appears in their list
- Based on their settings, it may be auto-approved or pending
- If notification fails, share the token manually as a fallback
When you create a sender connection:
- Your site calls
POST {remote_url}/{prefix}/sync/api/register-connection - The remote site creates a matching receiver connection
- Based on their incoming mode:
- Auto Accept: Ready to use immediately
- Require Approval: Admin must approve in their connections list
- Require Password: You need to provide their password
- Deny All: Connection is rejected
alias PhoenixKitSync.Connections
# Create a sender connection. The raw token is returned in the third
# tuple element — it's only available at creation time; afterwards only
# the SHA-256 hash is stored.
{:ok, connection, token} =
Connections.create_connection(%{
"name" => "Production Backup",
"direction" => "sender",
"site_url" => "https://backup.example.com",
"approval_mode" => "auto_approve",
"allowed_tables" => ["users", "posts"],
"max_downloads" => 100,
"created_by_uuid" => current_user.uuid
})
# Approve a pending connection (admin_user_uuid is recorded as the actor
# on the resulting `sync.connection.approved` activity-log row).
{:ok, connection} = Connections.approve_connection(connection, admin_user_uuid)
# Suspend a connection
{:ok, connection} = Connections.suspend_connection(connection, admin_user_uuid, "Security audit")
# Reactivate a suspended connection. Pass `actor_uuid:` in opts so the
# activity log records who reactivated it.
{:ok, connection} = Connections.reactivate_connection(connection, actor_uuid: admin_user_uuid)
# Revoke permanently
{:ok, connection} = Connections.revoke_connection(connection, admin_user_uuid, "No longer needed")
# Rotate the auth token (returns the new raw token, hash stored in DB).
# Logged as `sync.connection.token_regenerated`.
{:ok, connection, new_token} = Connections.regenerate_token(connection, actor_uuid: admin_user_uuid)
# Validate a token (used by receiver when connecting)
case Connections.validate_connection(token, client_ip) do
{:ok, connection} -> # Token is valid, connection is active
{:error, :invalid_token} -> # Token doesn't exist
{:error, :connection_expired} -> # Expired or revoked
{:error, :download_limit_reached} -> # Max downloads reached
{:error, :ip_not_allowed} -> # IP not in whitelist
{:error, :outside_allowed_hours} -> # Outside time window
end
alias PhoenixKitSync.Transfers
# Record a transfer
{:ok, transfer} = Transfers.create_transfer(%{
direction: "send",
connection_uuid: connection.uuid,
table_name: "users",
records_transferred: 150,
bytes_transferred: 45000,
status: "completed"
})
# Get transfer history
transfers = Transfers.list_transfers(
connection_uuid: connection.uuid,
direction: "send",
status: "completed"
)
# Get statistics for a connection
stats = Transfers.connection_stats(connection.uuid)
# => %{total_transfers: 25, total_records: 5000, total_bytes: 1500000}# Enable/disable
PhoenixKitSync.enabled?()
PhoenixKitSync.enable_system()
PhoenixKitSync.disable_system()
PhoenixKitSync.get_config()
# Local database inspection
{:ok, tables} = PhoenixKitSync.list_tables()
{:ok, schema} = PhoenixKitSync.get_schema("users")
{:ok, records} = PhoenixKitSync.export_records("users", limit: 100)
# Import with conflict strategy
{:ok, result} = PhoenixKitSync.import_records("users", records, :skip)alias PhoenixKitSync.Client
{:ok, client} = Client.connect("https://sender.com", "ABC12345")
{:ok, tables} = Client.list_tables(client)
{:ok, result} = Client.transfer(client, "users", strategy: :skip)
Client.disconnect(client)Cross-site communication endpoints (under the configured URL prefix):
POST /sync/api/register-connection— Register incoming connectionPOST /sync/api/delete-connection— Delete a connectionPOST /sync/api/verify-connection— Verify connection tokenPOST /sync/api/update-status— Update connection statusPOST /sync/api/get-connection-status— Query connection statusPOST /sync/api/list-tables— List available tablesPOST /sync/api/pull-data— Pull table dataPOST /sync/api/table-schema— Get table schemaPOST /sync/api/table-records— Get table recordsGET /sync/api/status— Check module status
Table migrations are currently managed by PhoenixKit's core migration system.
If the tables don't already exist, this package can create them automatically
via PhoenixKitSync.Migration.up/0.
See docs/table_structure.md in the source repository for full schema documentation.
Connections have fields for auto-sync but the scheduler isn't implemented yet:
auto_sync_enabled: Enable automatic periodic syncauto_sync_tables: Tables to sync automaticallyauto_sync_interval_minutes: How often to sync
- Token Security: Tokens are SHA-256 hashed in the database and only shown once on creation; comparisons use
Plug.Crypto.secure_compare/2to defeat timing attacks - Per-connection table authorization: Even with a valid token, every API endpoint that returns or accepts table data (
list_tables,pull_data,table_schema,table_records) filters through the connection'sexcluded_tablesblocklist andallowed_tablesallowlist - Parameterised SQL with identifier validation: Every dynamic table / column name interpolated into raw SQL passes through
SchemaInspector.valid_identifier?/1first; values always go through$Nparameterised binds - Optional Password: Additional password can be required per-transfer; refuses registration outright when
incoming_mode = "require_password"but no password is configured (no silent bypass) - IP Whitelisting: Restrict connections to specific IP addresses
- Time Restrictions: Allow connections only during specific hours
- Rate Limiting: Per-connection download / record limits enforced by
Connections.validate_connection/2 - Audit Trail: Every state-changing operation lands a row in
phoenix_kit_activitieswithactor_uuid,resource_type,resource_uuid, and a PII-safe metadata subset (nosite_url, noauth_token_hash)
- Verify the token is correct and hasn't been regenerated
- Check connection status is "active"
- Verify IP is in whitelist (if configured)
- Check time-of-day restrictions
- Verify download/record limits haven't been exceeded
- Check transfer history for error messages
- Verify table is in allowed tables (if configured)
- Check approval status if approval mode is enabled
- Review server logs for detailed errors