socialdept / atp-parity
AT Protocol record mapping and sync for Laravel Eloquent models
Installs: 23
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/socialdept/atp-parity
Requires
- php: ^8.2
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- socialdept/atp-client: ^0.0
- socialdept/atp-resolver: ^1.1
- socialdept/atp-schema: ^0.3
- socialdept/atp-signals: ^1.2
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
Bidirectional mapping between AT Protocol records and Laravel Eloquent models.
What is Parity?
Parity is a Laravel package that bridges your Eloquent models with AT Protocol records. It provides bidirectional mapping, automatic firehose synchronization, and type-safe transformations between your database and the decentralized social web.
Think of it as Laravel's model casts, but for AT Protocol records.
Why use Parity?
- Laravel-style code - Familiar patterns you already know
- Bidirectional mapping - Transform records to models and back
- Firehose sync - Automatically sync network events to your database
- Type-safe DTOs - Full integration with atp-schema generated types
- Model traits - Add AT Protocol awareness to any Eloquent model
- Flexible mappers - Define custom transformations for your domain
- Blob handling - Download, upload, and serve images and videos
Quick Example
use SocialDept\AtpParity\RecordMapper; use SocialDept\AtpSchema\Data\Data; use Illuminate\Database\Eloquent\Model; class PostMapper extends RecordMapper { public function recordClass(): string { return \SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post::class; } public function modelClass(): string { return \App\Models\Post::class; } protected function recordToAttributes(Data $record): array { return [ 'content' => $record->text, 'published_at' => $record->createdAt, ]; } protected function modelToRecordData(Model $model): array { return [ 'text' => $model->content, 'createdAt' => $model->published_at->toIso8601String(), ]; } }
Installation
composer require socialdept/atp-parity
Optionally publish the configuration:
php artisan vendor:publish --tag=parity-config
Getting Started
Once installed, you're three steps away from syncing AT Protocol records:
1. Create a Mapper
Define how your record maps to your model:
class PostMapper extends RecordMapper { public function recordClass(): string { return Post::class; // Your atp-schema DTO or custom Record } public function modelClass(): string { return \App\Models\Post::class; } protected function recordToAttributes(Data $record): array { return ['content' => $record->text]; } protected function modelToRecordData(Model $model): array { return ['text' => $model->content]; } }
2. Register Your Mapper
// config/parity.php return [ 'mappers' => [ App\AtpMappers\PostMapper::class, ], ];
3. Add the Trait to Your Model
use SocialDept\AtpParity\Concerns\HasAtpRecord; class Post extends Model { use HasAtpRecord; }
Your model can now convert to/from AT Protocol records and query by URI.
What can you build?
- Data mirrors - Keep local copies of AT Protocol data
- AppViews - Build custom applications with synced data
- Analytics platforms - Store and analyze network activity
- Content aggregators - Collect and organize posts locally
- Moderation tools - Track and manage content in your database
- Hybrid applications - Combine local and federated data
Ecosystem Integration
Parity is designed to work seamlessly with the other atp-* packages:
| Package | Integration |
|---|---|
| atp-schema | Records extend Data, use generated DTOs directly |
| atp-client | RecordHelper for fetching and hydrating records |
| atp-signals | ParitySignal for automatic firehose sync |
Using with atp-schema
Use generated schema classes directly with SchemaMapper:
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; use SocialDept\AtpParity\Support\SchemaMapper; $mapper = new SchemaMapper( schemaClass: Post::class, modelClass: \App\Models\Post::class, toAttributes: fn(Post $p) => [ 'content' => $p->text, 'published_at' => $p->createdAt, ], toRecordData: fn($m) => [ 'text' => $m->content, 'createdAt' => $m->published_at->toIso8601String(), ], ); $registry->register($mapper);
Using with atp-client
Fetch records by URI and convert directly to models:
use SocialDept\AtpParity\Support\RecordHelper; $helper = app(RecordHelper::class); // Fetch as typed DTO $record = $helper->fetch('at://did:plc:xxx/app.bsky.feed.post/abc123'); // Fetch and convert to model (unsaved) $post = $helper->fetchAsModel('at://did:plc:xxx/app.bsky.feed.post/abc123'); // Fetch and sync to database (upsert) $post = $helper->sync('at://did:plc:xxx/app.bsky.feed.post/abc123');
The helper automatically resolves the DID to find the correct PDS endpoint, so it works with any AT Protocol server - not just Bluesky.
Using with atp-signals
Enable automatic firehose synchronization by registering the ParitySignal:
// config/signal.php return [ 'signals' => [ \SocialDept\AtpParity\Signals\ParitySignal::class, ], ];
Run php artisan signal:consume and your models will automatically sync with matching firehose events.
Importing Historical Data
For existing records created before you started consuming the firehose:
# Import a user's records php artisan parity:import did:plc:z72i7hdynmk6r22z27h6tvur # Check import status php artisan parity:import-status
Or programmatically:
use SocialDept\AtpParity\Import\ImportService; $service = app(ImportService::class); $result = $service->importUser('did:plc:z72i7hdynmk6r22z27h6tvur'); echo "Synced {$result->recordsSynced} records";
Documentation
For detailed documentation on specific topics:
- Record Mappers - Creating and using mappers
- Model Traits - HasAtpRecord and SyncsWithAtp
- Automatic Syncing - Auto-sync models with AT Protocol
- Blob Handling - Downloading, uploading, and serving blobs
- atp-schema Integration - Using generated DTOs
- atp-client Integration - RecordHelper and fetching
- atp-signals Integration - ParitySignal and firehose sync
- Importing - Syncing historical data
Model Traits
HasAtpRecord
Add AT Protocol awareness to your models:
use SocialDept\AtpParity\Concerns\HasAtpRecord; class Post extends Model { use HasAtpRecord; protected $fillable = ['content', 'atp_uri', 'atp_cid']; }
Available methods:
// Get AT Protocol metadata $post->getAtpUri(); // at://did:plc:xxx/app.bsky.feed.post/rkey $post->getAtpCid(); // bafyre... $post->getAtpDid(); // did:plc:xxx (extracted from URI) $post->getAtpCollection(); // app.bsky.feed.post (extracted from URI) $post->getAtpRkey(); // rkey (extracted from URI) // Check sync status $post->hasAtpRecord(); // true if synced // Convert to record DTO $record = $post->toAtpRecord(); // Query scopes Post::withAtpRecord()->get(); // Only synced posts Post::withoutAtpRecord()->get(); // Only unsynced posts Post::whereAtpUri($uri)->first(); // Find by URI
SyncsWithAtp
Extended trait for bidirectional sync tracking:
use SocialDept\AtpParity\Concerns\SyncsWithAtp; class Post extends Model { use SyncsWithAtp; }
Additional methods:
// Track sync status $post->getAtpSyncedAt(); // Last sync timestamp $post->hasLocalChanges(); // True if updated since last sync // Mark as synced $post->markAsSynced($uri, $cid); // Update from remote $post->updateFromRecord($record, $uri, $cid);
HasAtpBlobs
Add blob handling to models with images or other binary content:
use SocialDept\AtpParity\Concerns\HasAtpRecord; use SocialDept\AtpParity\Concerns\HasAtpBlobs; class Post extends Model { use HasAtpRecord, HasAtpBlobs; protected $casts = ['atp_blobs' => 'array']; }
Available methods:
// Get URLs for blobs $url = $post->getAtpBlobUrl('avatar'); // Single blob URL $urls = $post->getAtpBlobUrls('images'); // Array of URLs // Download blobs locally $post->downloadAtpBlobs(); // Check status $post->hasAtpBlobs(); // Has any blob data $post->hasLocalBlobs(); // All blobs downloaded locally
See Blob Handling for complete documentation including MediaLibrary integration.
AutoSyncsWithAtp
Automatically sync models with AT Protocol on create, update, and delete:
use SocialDept\AtpParity\Concerns\AutoSyncsWithAtp; class Post extends Model { use AutoSyncsWithAtp; public function syncAsDid(): ?string { return $this->user->did; } public function shouldAutoSync(): bool { return $this->status === 'published'; } }
When enabled, the model automatically syncs:
$post = Post::create(['content' => 'Hello!']); // Syncs to ATP $post->update(['content' => 'Updated']); // Updates ATP record $post->delete(); // Removes from ATP
See Automatic Syncing for complete documentation.
Database Migration
Add AT Protocol columns to your models:
Schema::table('posts', function (Blueprint $table) { $table->string('atp_uri')->nullable()->unique(); $table->string('atp_cid')->nullable(); $table->timestamp('atp_synced_at')->nullable(); // For SyncsWithAtp $table->json('atp_blobs')->nullable(); // For HasAtpBlobs });
Publish and run Parity's migrations for import state tracking and blob mappings:
php artisan vendor:publish --tag=parity-migrations php artisan migrate
Note: The parity_blob_mappings migration is only required if using the filesystem storage driver. If using medialibrary mode, you can skip this migration.
Configuration
// config/parity.php return [ // Registered mappers 'mappers' => [ App\AtpMappers\PostMapper::class, App\AtpMappers\ProfileMapper::class, ], // Column names for AT Protocol metadata 'columns' => [ 'uri' => 'atp_uri', 'cid' => 'atp_cid', ], // Blob handling configuration 'blobs' => [ // 'filesystem' (requires migrations) or 'medialibrary' (no extra migrations) 'storage_driver' => \SocialDept\AtpParity\Enums\BlobStorageDriver::Filesystem, 'download_on_import' => env('PARITY_BLOB_DOWNLOAD', false), 'disk' => env('PARITY_BLOB_DISK', 'local'), 'url_strategy' => \SocialDept\AtpParity\Enums\BlobUrlStrategy::Cdn, ], ];
Creating Custom Records
Extend the Record base class for custom AT Protocol records:
use SocialDept\AtpParity\Data\Record; use Carbon\Carbon; class PostRecord extends Record { public function __construct( public readonly string $text, public readonly Carbon $createdAt, public readonly ?array $facets = null, ) {} public static function getLexicon(): string { return 'app.bsky.feed.post'; } public static function fromArray(array $data): static { return new static( text: $data['text'], createdAt: Carbon::parse($data['createdAt']), facets: $data['facets'] ?? null, ); } }
The Record class extends atp-schema's Data and implements atp-client's Recordable interface, ensuring full compatibility with the ecosystem.
Requirements
- PHP 8.2+
- Laravel 10, 11, or 12
- socialdept/atp-schema ^0.3
- socialdept/atp-client ^0.0
- socialdept/atp-resolver ^1.1
- socialdept/atp-signals ^1.1
Testing
composer test
Resources
- AT Protocol Documentation
- Bluesky API Docs
- atp-schema - Generated AT Protocol DTOs
- atp-client - AT Protocol HTTP client
- atp-signals - Firehose event consumer
Support & Contributing
Found a bug or have a feature request? Open an issue.
Want to contribute? Check out the contribution guidelines.
Changelog
Please see changelog for recent changes.
Credits
- Miguel Batres - founder & lead maintainer
- All contributors
License
Parity is open-source software licensed under the MIT license.
Built for the Federation - By Social Dept.
