Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
PHP 100.0%
24 2 2

Clone this repository

https://tangled.org/socialde.pt/atp-parity
git@tangled.org:socialde.pt/atp-parity

For self-hosted knots, clone URLs may differ based on your setup.

README.md

Parity Header

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

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:

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);

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
});

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',
    ],
];

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#

Testing#

composer test

Resources#

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#

License#

Parity is open-source software licensed under the MIT license.


Built for the Federation - By Social Dept.