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

v0.2.10 2026-01-10 15:50 UTC

README

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
  • 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:

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

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.