···11-# AtpReplicator
11+[](https://github.com/socialdept/atp-parity)
22+33+<h3 align="center">
44+ Bidirectional mapping between AT Protocol records and Laravel Eloquent models.
55+</h3>
66+77+<p align="center">
88+ <br>
99+ <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>
1010+ <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>
1111+ <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>
1212+ <a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-parity?style=flat-square"></a>
1313+</p>
1414+1515+---
1616+1717+## What is Parity?
1818+1919+**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.
2020+2121+Think of it as Laravel's model casts, but for AT Protocol records.
2222+2323+## Why use Parity?
22433-[![Latest Version on Packagist][ico-version]][link-packagist]
44-[![Total Downloads][ico-downloads]][link-downloads]
55-[![Build Status][ico-travis]][link-travis]
66-[![StyleCI][ico-styleci]][link-styleci]
2525+- **Laravel-style code** - Familiar patterns you already know
2626+- **Bidirectional mapping** - Transform records to models and back
2727+- **Firehose sync** - Automatically sync network events to your database
2828+- **Type-safe DTOs** - Full integration with atp-schema generated types
2929+- **Model traits** - Add AT Protocol awareness to any Eloquent model
3030+- **Flexible mappers** - Define custom transformations for your domain
3131+3232+## Quick Example
3333+3434+```php
3535+use SocialDept\AtpParity\RecordMapper;
3636+use SocialDept\AtpSchema\Data\Data;
3737+use Illuminate\Database\Eloquent\Model;
3838+3939+class PostMapper extends RecordMapper
4040+{
4141+ public function recordClass(): string
4242+ {
4343+ return \SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post::class;
4444+ }
4545+4646+ public function modelClass(): string
4747+ {
4848+ return \App\Models\Post::class;
4949+ }
5050+5151+ protected function recordToAttributes(Data $record): array
5252+ {
5353+ return [
5454+ 'content' => $record->text,
5555+ 'published_at' => $record->createdAt,
5656+ ];
5757+ }
75888-This is where your description should go. Take a look at [contributing.md](contributing.md) to see a to do list.
5959+ protected function modelToRecordData(Model $model): array
6060+ {
6161+ return [
6262+ 'text' => $model->content,
6363+ 'createdAt' => $model->published_at->toIso8601String(),
6464+ ];
6565+ }
6666+}
6767+```
9681069## Installation
11701212-Via Composer
7171+```bash
7272+composer require socialdept/atp-parity
7373+```
7474+7575+Optionally publish the configuration:
7676+7777+```bash
7878+php artisan vendor:publish --tag=parity-config
7979+```
8080+8181+## Getting Started
8282+8383+Once installed, you're three steps away from syncing AT Protocol records:
8484+8585+### 1. Create a Mapper
8686+8787+Define how your record maps to your model:
8888+8989+```php
9090+class PostMapper extends RecordMapper
9191+{
9292+ public function recordClass(): string
9393+ {
9494+ return Post::class; // Your atp-schema DTO or custom Record
9595+ }
9696+9797+ public function modelClass(): string
9898+ {
9999+ return \App\Models\Post::class;
100100+ }
101101+102102+ protected function recordToAttributes(Data $record): array
103103+ {
104104+ return ['content' => $record->text];
105105+ }
106106+107107+ protected function modelToRecordData(Model $model): array
108108+ {
109109+ return ['text' => $model->content];
110110+ }
111111+}
112112+```
113113+114114+### 2. Register Your Mapper
115115+116116+```php
117117+// config/parity.php
118118+return [
119119+ 'mappers' => [
120120+ App\AtpMappers\PostMapper::class,
121121+ ],
122122+];
123123+```
124124+125125+### 3. Add the Trait to Your Model
126126+127127+```php
128128+use SocialDept\AtpParity\Concerns\HasAtpRecord;
129129+130130+class Post extends Model
131131+{
132132+ use HasAtpRecord;
133133+}
134134+```
135135+136136+Your model can now convert to/from AT Protocol records and query by URI.
137137+138138+## What can you build?
139139+140140+- **Data mirrors** - Keep local copies of AT Protocol data
141141+- **AppViews** - Build custom applications with synced data
142142+- **Analytics platforms** - Store and analyze network activity
143143+- **Content aggregators** - Collect and organize posts locally
144144+- **Moderation tools** - Track and manage content in your database
145145+- **Hybrid applications** - Combine local and federated data
146146+147147+## Ecosystem Integration
148148+149149+Parity is designed to work seamlessly with the other atp-* packages:
150150+151151+| Package | Integration |
152152+|---------|-------------|
153153+| **atp-schema** | Records extend `Data`, use generated DTOs directly |
154154+| **atp-client** | `RecordHelper` for fetching and hydrating records |
155155+| **atp-signals** | `ParitySignal` for automatic firehose sync |
156156+157157+### Using with atp-schema
158158+159159+Use generated schema classes directly with `SchemaMapper`:
160160+161161+```php
162162+use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
163163+use SocialDept\AtpParity\Support\SchemaMapper;
164164+165165+$mapper = new SchemaMapper(
166166+ schemaClass: Post::class,
167167+ modelClass: \App\Models\Post::class,
168168+ toAttributes: fn(Post $p) => [
169169+ 'content' => $p->text,
170170+ 'published_at' => $p->createdAt,
171171+ ],
172172+ toRecordData: fn($m) => [
173173+ 'text' => $m->content,
174174+ 'createdAt' => $m->published_at->toIso8601String(),
175175+ ],
176176+);
177177+178178+$registry->register($mapper);
179179+```
180180+181181+### Using with atp-client
182182+183183+Fetch records by URI and convert directly to models:
184184+185185+```php
186186+use SocialDept\AtpParity\Support\RecordHelper;
187187+188188+$helper = app(RecordHelper::class);
189189+190190+// Fetch as typed DTO
191191+$record = $helper->fetch('at://did:plc:xxx/app.bsky.feed.post/abc123');
192192+193193+// Fetch and convert to model (unsaved)
194194+$post = $helper->fetchAsModel('at://did:plc:xxx/app.bsky.feed.post/abc123');
195195+196196+// Fetch and sync to database (upsert)
197197+$post = $helper->sync('at://did:plc:xxx/app.bsky.feed.post/abc123');
198198+```
199199+200200+The helper automatically resolves the DID to find the correct PDS endpoint, so it works with any AT Protocol server - not just Bluesky.
201201+202202+### Using with atp-signals
203203+204204+Enable automatic firehose synchronization by registering the `ParitySignal`:
205205+206206+```php
207207+// config/signal.php
208208+return [
209209+ 'signals' => [
210210+ \SocialDept\AtpParity\Signals\ParitySignal::class,
211211+ ],
212212+];
213213+```
214214+215215+Run `php artisan signal:consume` and your models will automatically sync with matching firehose events.
216216+217217+### Importing Historical Data
218218+219219+For existing records created before you started consuming the firehose:
1322014221```bash
1515-composer require socialdept/atp-parity
222222+# Import a user's records
223223+php artisan parity:import did:plc:z72i7hdynmk6r22z27h6tvur
224224+225225+# Check import status
226226+php artisan parity:import-status
227227+```
228228+229229+Or programmatically:
230230+231231+```php
232232+use SocialDept\AtpParity\Import\ImportService;
233233+234234+$service = app(ImportService::class);
235235+$result = $service->importUser('did:plc:z72i7hdynmk6r22z27h6tvur');
236236+237237+echo "Synced {$result->recordsSynced} records";
16238```
172391818-## Usage
240240+## Documentation
241241+242242+For detailed documentation on specific topics:
243243+244244+- [Record Mappers](docs/mappers.md) - Creating and using mappers
245245+- [Model Traits](docs/traits.md) - HasAtpRecord and SyncsWithAtp
246246+- [atp-schema Integration](docs/atp-schema-integration.md) - Using generated DTOs
247247+- [atp-client Integration](docs/atp-client-integration.md) - RecordHelper and fetching
248248+- [atp-signals Integration](docs/atp-signals-integration.md) - ParitySignal and firehose sync
249249+- [Importing](docs/importing.md) - Syncing historical data
192502020-## Change log
251251+## Model Traits
212522222-Please see the [changelog](changelog.md) for more information on what has changed recently.
253253+### HasAtpRecord
254254+255255+Add AT Protocol awareness to your models:
256256+257257+```php
258258+use SocialDept\AtpParity\Concerns\HasAtpRecord;
259259+260260+class Post extends Model
261261+{
262262+ use HasAtpRecord;
263263+264264+ protected $fillable = ['content', 'atp_uri', 'atp_cid'];
265265+}
266266+```
267267+268268+Available methods:
269269+270270+```php
271271+// Get AT Protocol metadata
272272+$post->getAtpUri(); // at://did:plc:xxx/app.bsky.feed.post/rkey
273273+$post->getAtpCid(); // bafyre...
274274+$post->getAtpDid(); // did:plc:xxx (extracted from URI)
275275+$post->getAtpCollection(); // app.bsky.feed.post (extracted from URI)
276276+$post->getAtpRkey(); // rkey (extracted from URI)
277277+278278+// Check sync status
279279+$post->hasAtpRecord(); // true if synced
280280+281281+// Convert to record DTO
282282+$record = $post->toAtpRecord();
283283+284284+// Query scopes
285285+Post::withAtpRecord()->get(); // Only synced posts
286286+Post::withoutAtpRecord()->get(); // Only unsynced posts
287287+Post::whereAtpUri($uri)->first(); // Find by URI
288288+```
289289+290290+### SyncsWithAtp
291291+292292+Extended trait for bidirectional sync tracking:
293293+294294+```php
295295+use SocialDept\AtpParity\Concerns\SyncsWithAtp;
296296+297297+class Post extends Model
298298+{
299299+ use SyncsWithAtp;
300300+}
301301+```
302302+303303+Additional methods:
304304+305305+```php
306306+// Track sync status
307307+$post->getAtpSyncedAt(); // Last sync timestamp
308308+$post->hasLocalChanges(); // True if updated since last sync
309309+310310+// Mark as synced
311311+$post->markAsSynced($uri, $cid);
312312+313313+// Update from remote
314314+$post->updateFromRecord($record, $uri, $cid);
315315+```
316316+317317+## Database Migration
318318+319319+Add AT Protocol columns to your models:
320320+321321+```php
322322+Schema::table('posts', function (Blueprint $table) {
323323+ $table->string('atp_uri')->nullable()->unique();
324324+ $table->string('atp_cid')->nullable();
325325+ $table->timestamp('atp_synced_at')->nullable(); // For SyncsWithAtp
326326+});
327327+```
328328+329329+## Configuration
330330+331331+```php
332332+// config/parity.php
333333+return [
334334+ // Registered mappers
335335+ 'mappers' => [
336336+ App\AtpMappers\PostMapper::class,
337337+ App\AtpMappers\ProfileMapper::class,
338338+ ],
339339+340340+ // Column names for AT Protocol metadata
341341+ 'columns' => [
342342+ 'uri' => 'atp_uri',
343343+ 'cid' => 'atp_cid',
344344+ ],
345345+];
346346+```
347347+348348+## Creating Custom Records
349349+350350+Extend the `Record` base class for custom AT Protocol records:
351351+352352+```php
353353+use SocialDept\AtpParity\Data\Record;
354354+use Carbon\Carbon;
355355+356356+class PostRecord extends Record
357357+{
358358+ public function __construct(
359359+ public readonly string $text,
360360+ public readonly Carbon $createdAt,
361361+ public readonly ?array $facets = null,
362362+ ) {}
363363+364364+ public static function getLexicon(): string
365365+ {
366366+ return 'app.bsky.feed.post';
367367+ }
368368+369369+ public static function fromArray(array $data): static
370370+ {
371371+ return new static(
372372+ text: $data['text'],
373373+ createdAt: Carbon::parse($data['createdAt']),
374374+ facets: $data['facets'] ?? null,
375375+ );
376376+ }
377377+}
378378+```
379379+380380+The `Record` class extends `atp-schema`'s `Data` and implements `atp-client`'s `Recordable` interface, ensuring full compatibility with the ecosystem.
381381+382382+## Requirements
383383+384384+- PHP 8.2+
385385+- Laravel 10, 11, or 12
386386+- [socialdept/atp-schema](https://github.com/socialdept/atp-schema) ^0.3
387387+- [socialdept/atp-client](https://github.com/socialdept/atp-client) ^0.0
388388+- [socialdept/atp-resolver](https://github.com/socialdept/atp-resolver) ^1.1
389389+- [socialdept/atp-signals](https://github.com/socialdept/atp-signals) ^1.1
2339024391## Testing
25392···27394composer test
28395```
293963030-## Contributing
397397+## Resources
398398+399399+- [AT Protocol Documentation](https://atproto.com/)
400400+- [Bluesky API Docs](https://docs.bsky.app/)
401401+- [atp-schema](https://github.com/socialdept/atp-schema) - Generated AT Protocol DTOs
402402+- [atp-client](https://github.com/socialdept/atp-client) - AT Protocol HTTP client
403403+- [atp-signals](https://github.com/socialdept/atp-signals) - Firehose event consumer
404404+405405+## Support & Contributing
406406+407407+Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-parity/issues).
314083232-Please see [contributing.md](contributing.md) for details and a todolist.
409409+Want to contribute? Check out the [contribution guidelines](contributing.md).
334103434-## Security
411411+## Changelog
354123636-If you discover any security related issues, please email author@email.com instead of using the issue tracker.
413413+Please see [changelog](changelog.md) for recent changes.
3741438415## Credits
394164040-- [Author Name][link-author]
4141-- [All Contributors][link-contributors]
417417+- [Miguel Batres](https://batres.co) - founder & lead maintainer
418418+- [All contributors](https://github.com/socialdept/atp-parity/graphs/contributors)
4241943420## License
444214545-MIT. Please see the [license file](license.md) for more information.
422422+Parity is open-source software licensed under the [MIT license](license.md).
464234747-[ico-version]: https://img.shields.io/packagist/v/socialdept/atp-parity.svg?style=flat-square
4848-[ico-downloads]: https://img.shields.io/packagist/dt/socialdept/atp-parity.svg?style=flat-square
4949-[ico-travis]: https://img.shields.io/travis/socialdept/atp-parity/master.svg?style=flat-square
5050-[ico-styleci]: https://styleci.io/repos/12345678/shield
424424+---
514255252-[link-packagist]: https://packagist.org/packages/socialdept/atp-parity
5353-[link-downloads]: https://packagist.org/packages/socialdept/atp-parity
5454-[link-travis]: https://travis-ci.org/socialdept/atp-parity
5555-[link-styleci]: https://styleci.io/repos/12345678
5656-[link-author]: https://github.com/social-dept
5757-[link-contributors]: ../../contributors
426426+**Built for the Federation** - By Social Dept.