Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
1[](https://github.com/socialdept/atp-parity)
2
3<h3 align="center">
4 Bidirectional mapping between AT Protocol records and Laravel Eloquent models.
5</h3>
6
7<p align="center">
8 <br>
9 <a href="https://packagist.org/packages/socialdept/atp-parity" title="Latest Version on Packagist"><img src="https://img.shields.io/packagist/v/socialdept/atp-parity.svg?style=flat-square"></a>
10 <a href="https://packagist.org/packages/socialdept/atp-parity" title="Total Downloads"><img src="https://img.shields.io/packagist/dt/socialdept/atp-parity.svg?style=flat-square"></a>
11 <a href="https://github.com/socialdept/atp-parity/actions/workflows/tests.yml" title="GitHub Tests Action Status"><img src="https://img.shields.io/github/actions/workflow/status/socialdept/atp-parity/tests.yml?branch=main&label=tests&style=flat-square"></a>
12 <a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-parity?style=flat-square"></a>
13</p>
14
15---
16
17## What is Parity?
18
19**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.
20
21Think of it as Laravel's model casts, but for AT Protocol records.
22
23## Why use Parity?
24
25- **Laravel-style code** - Familiar patterns you already know
26- **Bidirectional mapping** - Transform records to models and back
27- **Firehose sync** - Automatically sync network events to your database
28- **Type-safe DTOs** - Full integration with atp-schema generated types
29- **Model traits** - Add AT Protocol awareness to any Eloquent model
30- **Flexible mappers** - Define custom transformations for your domain
31
32## Quick Example
33
34```php
35use SocialDept\AtpParity\RecordMapper;
36use SocialDept\AtpSchema\Data\Data;
37use Illuminate\Database\Eloquent\Model;
38
39class PostMapper extends RecordMapper
40{
41 public function recordClass(): string
42 {
43 return \SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post::class;
44 }
45
46 public function modelClass(): string
47 {
48 return \App\Models\Post::class;
49 }
50
51 protected function recordToAttributes(Data $record): array
52 {
53 return [
54 'content' => $record->text,
55 'published_at' => $record->createdAt,
56 ];
57 }
58
59 protected function modelToRecordData(Model $model): array
60 {
61 return [
62 'text' => $model->content,
63 'createdAt' => $model->published_at->toIso8601String(),
64 ];
65 }
66}
67```
68
69## Installation
70
71```bash
72composer require socialdept/atp-parity
73```
74
75Optionally publish the configuration:
76
77```bash
78php artisan vendor:publish --tag=parity-config
79```
80
81## Getting Started
82
83Once installed, you're three steps away from syncing AT Protocol records:
84
85### 1. Create a Mapper
86
87Define how your record maps to your model:
88
89```php
90class PostMapper extends RecordMapper
91{
92 public function recordClass(): string
93 {
94 return Post::class; // Your atp-schema DTO or custom Record
95 }
96
97 public function modelClass(): string
98 {
99 return \App\Models\Post::class;
100 }
101
102 protected function recordToAttributes(Data $record): array
103 {
104 return ['content' => $record->text];
105 }
106
107 protected function modelToRecordData(Model $model): array
108 {
109 return ['text' => $model->content];
110 }
111}
112```
113
114### 2. Register Your Mapper
115
116```php
117// config/parity.php
118return [
119 'mappers' => [
120 App\AtpMappers\PostMapper::class,
121 ],
122];
123```
124
125### 3. Add the Trait to Your Model
126
127```php
128use SocialDept\AtpParity\Concerns\HasAtpRecord;
129
130class Post extends Model
131{
132 use HasAtpRecord;
133}
134```
135
136Your model can now convert to/from AT Protocol records and query by URI.
137
138## What can you build?
139
140- **Data mirrors** - Keep local copies of AT Protocol data
141- **AppViews** - Build custom applications with synced data
142- **Analytics platforms** - Store and analyze network activity
143- **Content aggregators** - Collect and organize posts locally
144- **Moderation tools** - Track and manage content in your database
145- **Hybrid applications** - Combine local and federated data
146
147## Ecosystem Integration
148
149Parity is designed to work seamlessly with the other atp-* packages:
150
151| Package | Integration |
152|---------|-------------|
153| **atp-schema** | Records extend `Data`, use generated DTOs directly |
154| **atp-client** | `RecordHelper` for fetching and hydrating records |
155| **atp-signals** | `ParitySignal` for automatic firehose sync |
156
157### Using with atp-schema
158
159Use generated schema classes directly with `SchemaMapper`:
160
161```php
162use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
163use SocialDept\AtpParity\Support\SchemaMapper;
164
165$mapper = new SchemaMapper(
166 schemaClass: Post::class,
167 modelClass: \App\Models\Post::class,
168 toAttributes: fn(Post $p) => [
169 'content' => $p->text,
170 'published_at' => $p->createdAt,
171 ],
172 toRecordData: fn($m) => [
173 'text' => $m->content,
174 'createdAt' => $m->published_at->toIso8601String(),
175 ],
176);
177
178$registry->register($mapper);
179```
180
181### Using with atp-client
182
183Fetch records by URI and convert directly to models:
184
185```php
186use SocialDept\AtpParity\Support\RecordHelper;
187
188$helper = app(RecordHelper::class);
189
190// Fetch as typed DTO
191$record = $helper->fetch('at://did:plc:xxx/app.bsky.feed.post/abc123');
192
193// Fetch and convert to model (unsaved)
194$post = $helper->fetchAsModel('at://did:plc:xxx/app.bsky.feed.post/abc123');
195
196// Fetch and sync to database (upsert)
197$post = $helper->sync('at://did:plc:xxx/app.bsky.feed.post/abc123');
198```
199
200The helper automatically resolves the DID to find the correct PDS endpoint, so it works with any AT Protocol server - not just Bluesky.
201
202### Using with atp-signals
203
204Enable automatic firehose synchronization by registering the `ParitySignal`:
205
206```php
207// config/signal.php
208return [
209 'signals' => [
210 \SocialDept\AtpParity\Signals\ParitySignal::class,
211 ],
212];
213```
214
215Run `php artisan signal:consume` and your models will automatically sync with matching firehose events.
216
217### Importing Historical Data
218
219For existing records created before you started consuming the firehose:
220
221```bash
222# Import a user's records
223php artisan parity:import did:plc:z72i7hdynmk6r22z27h6tvur
224
225# Check import status
226php artisan parity:import-status
227```
228
229Or programmatically:
230
231```php
232use SocialDept\AtpParity\Import\ImportService;
233
234$service = app(ImportService::class);
235$result = $service->importUser('did:plc:z72i7hdynmk6r22z27h6tvur');
236
237echo "Synced {$result->recordsSynced} records";
238```
239
240## Documentation
241
242For detailed documentation on specific topics:
243
244- [Record Mappers](docs/mappers.md) - Creating and using mappers
245- [Model Traits](docs/traits.md) - HasAtpRecord and SyncsWithAtp
246- [atp-schema Integration](docs/atp-schema-integration.md) - Using generated DTOs
247- [atp-client Integration](docs/atp-client-integration.md) - RecordHelper and fetching
248- [atp-signals Integration](docs/atp-signals-integration.md) - ParitySignal and firehose sync
249- [Importing](docs/importing.md) - Syncing historical data
250
251## Model Traits
252
253### HasAtpRecord
254
255Add AT Protocol awareness to your models:
256
257```php
258use SocialDept\AtpParity\Concerns\HasAtpRecord;
259
260class Post extends Model
261{
262 use HasAtpRecord;
263
264 protected $fillable = ['content', 'atp_uri', 'atp_cid'];
265}
266```
267
268Available methods:
269
270```php
271// Get AT Protocol metadata
272$post->getAtpUri(); // at://did:plc:xxx/app.bsky.feed.post/rkey
273$post->getAtpCid(); // bafyre...
274$post->getAtpDid(); // did:plc:xxx (extracted from URI)
275$post->getAtpCollection(); // app.bsky.feed.post (extracted from URI)
276$post->getAtpRkey(); // rkey (extracted from URI)
277
278// Check sync status
279$post->hasAtpRecord(); // true if synced
280
281// Convert to record DTO
282$record = $post->toAtpRecord();
283
284// Query scopes
285Post::withAtpRecord()->get(); // Only synced posts
286Post::withoutAtpRecord()->get(); // Only unsynced posts
287Post::whereAtpUri($uri)->first(); // Find by URI
288```
289
290### SyncsWithAtp
291
292Extended trait for bidirectional sync tracking:
293
294```php
295use SocialDept\AtpParity\Concerns\SyncsWithAtp;
296
297class Post extends Model
298{
299 use SyncsWithAtp;
300}
301```
302
303Additional methods:
304
305```php
306// Track sync status
307$post->getAtpSyncedAt(); // Last sync timestamp
308$post->hasLocalChanges(); // True if updated since last sync
309
310// Mark as synced
311$post->markAsSynced($uri, $cid);
312
313// Update from remote
314$post->updateFromRecord($record, $uri, $cid);
315```
316
317## Database Migration
318
319Add AT Protocol columns to your models:
320
321```php
322Schema::table('posts', function (Blueprint $table) {
323 $table->string('atp_uri')->nullable()->unique();
324 $table->string('atp_cid')->nullable();
325 $table->timestamp('atp_synced_at')->nullable(); // For SyncsWithAtp
326});
327```
328
329## Configuration
330
331```php
332// config/parity.php
333return [
334 // Registered mappers
335 'mappers' => [
336 App\AtpMappers\PostMapper::class,
337 App\AtpMappers\ProfileMapper::class,
338 ],
339
340 // Column names for AT Protocol metadata
341 'columns' => [
342 'uri' => 'atp_uri',
343 'cid' => 'atp_cid',
344 ],
345];
346```
347
348## Creating Custom Records
349
350Extend the `Record` base class for custom AT Protocol records:
351
352```php
353use SocialDept\AtpParity\Data\Record;
354use Carbon\Carbon;
355
356class PostRecord extends Record
357{
358 public function __construct(
359 public readonly string $text,
360 public readonly Carbon $createdAt,
361 public readonly ?array $facets = null,
362 ) {}
363
364 public static function getLexicon(): string
365 {
366 return 'app.bsky.feed.post';
367 }
368
369 public static function fromArray(array $data): static
370 {
371 return new static(
372 text: $data['text'],
373 createdAt: Carbon::parse($data['createdAt']),
374 facets: $data['facets'] ?? null,
375 );
376 }
377}
378```
379
380The `Record` class extends `atp-schema`'s `Data` and implements `atp-client`'s `Recordable` interface, ensuring full compatibility with the ecosystem.
381
382## Requirements
383
384- PHP 8.2+
385- Laravel 10, 11, or 12
386- [socialdept/atp-schema](https://github.com/socialdept/atp-schema) ^0.3
387- [socialdept/atp-client](https://github.com/socialdept/atp-client) ^0.0
388- [socialdept/atp-resolver](https://github.com/socialdept/atp-resolver) ^1.1
389- [socialdept/atp-signals](https://github.com/socialdept/atp-signals) ^1.1
390
391## Testing
392
393```bash
394composer test
395```
396
397## Resources
398
399- [AT Protocol Documentation](https://atproto.com/)
400- [Bluesky API Docs](https://docs.bsky.app/)
401- [atp-schema](https://github.com/socialdept/atp-schema) - Generated AT Protocol DTOs
402- [atp-client](https://github.com/socialdept/atp-client) - AT Protocol HTTP client
403- [atp-signals](https://github.com/socialdept/atp-signals) - Firehose event consumer
404
405## Support & Contributing
406
407Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-parity/issues).
408
409Want to contribute? Check out the [contribution guidelines](contributing.md).
410
411## Changelog
412
413Please see [changelog](changelog.md) for recent changes.
414
415## Credits
416
417- [Miguel Batres](https://batres.co) - founder & lead maintainer
418- [All contributors](https://github.com/socialdept/atp-parity/graphs/contributors)
419
420## License
421
422Parity is open-source software licensed under the [MIT license](license.md).
423
424---
425
426**Built for the Federation** - By Social Dept.