PHPackages                             socialdept/atp-testnet - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. socialdept/atp-testnet

ActiveLibrary[Testing &amp; Quality](/categories/testing)

socialdept/atp-testnet
======================

Stand up a local AT Protocol testnet for integration testing in PHP

v0.2.1(3w ago)56MITPHPPHP ^8.3

Since Apr 11Pushed 3w agoCompare

[ Source](https://github.com/socialdept/atp-testnet)[ Packagist](https://packagist.org/packages/socialdept/atp-testnet)[ Docs](https://github.com/socialdept/atp-testnet)[ RSS](/packages/socialdept-atp-testnet/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (6)Versions (7)Used By (0)

[![Testnet Header](./header.png)](https://github.com/socialdept/atp-testnet)

###  Stand up a local AT Protocol testnet for integration testing in PHP.

[](#----stand-up-a-local-at-protocol-testnet-for-integration-testing-in-php)

 [![](https://camo.githubusercontent.com/fa355eaf4476519c3266aee38181d3d10d9d88f1525f234b436a1b428dbfad97/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f736f6369616c646570742f6174702d746573746e65742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/socialdept/atp-testnet "Latest Version on Packagist") [![](https://camo.githubusercontent.com/2f79336c98b42f77b57c6f99a8dc09cfa4852537407d00754bb8ff4191c63848/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f736f6369616c646570742f6174702d746573746e65742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/socialdept/atp-testnet "Total Downloads") [![](https://camo.githubusercontent.com/e7d5327ea9ae3e76c2bb1f0b21d7c1795b0a8aa5c39538384160259b250335b9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f736f6369616c646570742f6174702d746573746e65743f7374796c653d666c61742d737175617265)](LICENSE "Software License")

---

What is ATP Testnet?
--------------------

[](#what-is-atp-testnet)

**ATP Testnet** spins up a complete local AT Protocol network using Docker Compose for integration testing. No mocks, no fakes — real PLC directory, real PDS, real relay with firehose.

Quick Example
-------------

[](#quick-example)

```
use SocialDept\AtpTestnet\Testnet;

// Boot the network
$testnet = Testnet::start();

// Create an account
$alice = $testnet->createAccount('alice');
echo $alice->did;    // did:plc:abc123...
echo $alice->handle; // alice.test

// Create a record
$testnet->pds()->createRecord(
    'app.bsky.feed.post',
    ['$type' => 'app.bsky.feed.post', 'text' => 'Hello from testnet!', 'createdAt' => gmdate('c')],
    $alice->accessJwt,
);

// Query the PLC directory
$doc = $testnet->plc()->getDocument($alice->did);

// Subscribe to the firehose
$testnet->requestRelayCrawl();
$frames = $testnet->relay()->consumeFirehose(timeoutMs: 3000);

// Tear down
$testnet->stop();
```

Installation
------------

[](#installation)

```
composer require socialdept/atp-testnet --dev
```

Requirements
------------

[](#requirements)

- PHP 8.3+
- Docker &amp; Docker Compose
- ~2 GB disk for Docker images on first build

Building Docker Images
----------------------

[](#building-docker-images)

The PLC directory and relay are built from source since they don't publish official Docker images. Images are built automatically on the first `Testnet::start()` call, or you can pre-build them:

```
# Build missing images (skips if already built)
composer testnet:build

# Force rebuild all images
composer testnet:rebuild

# Or use the binary directly
vendor/bin/testnet-build
vendor/bin/testnet-build --rebuild
```

The first build clones the repos and compiles Go and Node — this takes a few minutes. After that, starts are instant.

The relay image is patched during build to work in Docker's internal network (SSRF bypass, hostname validation, domain ban checks, and account host matching are all disabled via the `RELAY_DISABLE_SSRF` environment variable).

The PLC image is patched to support disabling rate limits via the `PLC_DISABLE_RATE_LIMIT` environment variable (enabled by default in the testnet compose file). The production PLC enforces 10 ops/hour per DID — this patch bypasses that for local testing.

If you update the package and the build-time patches change, force a rebuild with `composer testnet:rebuild` or `vendor/bin/testnet-build --rebuild`.

Services
--------

[](#services)

ServiceImageDefault PortPurposePLC DirectoryBuilt from [did-method-plc](https://github.com/did-method-plc/did-method-plc)7100DID registration and PLC operationsPDS`ghcr.io/bluesky-social/pds:0.4`7102Account creation, repos, XRPCRelayBuilt from [indigo](https://github.com/bluesky-social/indigo)7101Firehose relay, crawls PDSEach service also runs its own Postgres instance internally.

Usage with PHPUnit / Pest
-------------------------

[](#usage-with-phpunit--pest)

```
use SocialDept\AtpTestnet\Concerns\UsesTestnet;

// PHPUnit
class MyTest extends TestCase
{
    use UsesTestnet;

    public function test_account_creation(): void
    {
        $account = $this->testnet->createAccount('bob');
        $this->assertStringStartsWith('did:plc:', $account->did);
    }
}

// Pest
uses(UsesTestnet::class);

test('can create account', function () {
    $account = $this->testnet->createAccount('bob');
    expect($account->did)->toStartWith('did:plc:');
});
```

The `UsesTestnet` trait boots the testnet once per test class and tears it down after all tests complete.

Test Isolation
--------------

[](#test-isolation)

The testnet accumulates state across tests (accounts, DIDs, relay subscriptions). Use the reset methods to start each test with a clean slate:

```
// Reset everything before each test
beforeEach(function () {
    $this->testnet->resetAll();
});

// Or reset individual services
beforeEach(function () {
    $this->testnet->resetPds();   // Clear accounts and repos only
});
```

MethodWhat it clears`resetPds()`All accounts, repos, sessions, invite codes (SQLite truncate)`resetPlc()`All DIDs and operations (Postgres truncate)`resetRelay()`All host subscriptions, account tracking, persisted data`resetAll()`All of the aboveServices stay running and healthy after reset — no container restarts needed.

Configuration
-------------

[](#configuration)

```
use SocialDept\AtpTestnet\TestnetConfig;
use SocialDept\AtpTestnet\Testnet;

$testnet = Testnet::start(new TestnetConfig(
    plcPort: 8100,
    pdsPort: 8102,
    relayPort: 8101,
    adminPassword: 'custom-admin',
));
```

Docker Compose Overrides
------------------------

[](#docker-compose-overrides)

You can customize the testnet's Docker Compose configuration by publishing an override file to your project root:

```
cp vendor/socialdept/atp-testnet/stubs/docker-compose.testnet.yml docker-compose.testnet.yml
```

Edit the file to override any service configuration:

```
# docker-compose.testnet.yml
services:
  plc:
    environment:
      PLC_DISABLE_RATE_LIMIT: "0"  # Re-enable rate limits
  pds:
    environment:
      PDS_DEV_MODE: "0"
      PDS_HOSTNAME: custom.test
```

The testnet automatically detects `docker-compose.testnet.yml` in your working directory and merges it on top of the base configuration. Only specify values you want to change — everything else keeps its default.

> **No rebuild needed.** Override files are applied at runtime via Docker Compose's `-f` merge. Changes take effect on the next `Testnet::start()` — no image rebuild required. Image rebuilds are only needed when the package itself updates its build-time patches (PLC rate limit bypass, relay SSRF bypass).

You can also pass an explicit path via config:

```
$testnet = Testnet::start(new TestnetConfig(
    composeOverride: base_path('my-custom-overrides.yml'),
));
```

### Environment Variables

[](#environment-variables)

VariableServiceDefaultDescription`PLC_DISABLE_RATE_LIMIT`PLC`1`Disable the 10 ops/hour rate limit per DID`RELAY_DISABLE_SSRF`Relay`1`Disable SSRF protection for Docker networking`RELAY_ALLOW_INSECURE_HOSTS`Relay`1`Allow HTTP (non-TLS) host connections`PDS_DEV_MODE`PDS`1`Allow HTTP hostnames, disable SSRF protection`PDS_INVITE_REQUIRED`PDS`false`Require invite codes for account creation`PDS_HOSTNAME`PDS`pds.test`PDS hostname for handle domainsDisposable &amp; Bring-Your-Own PDS
-----------------------------------

[](#disposable--bring-your-own-pds)

The shared testnet PDS uses fixed secrets and is reused across tests — fine for PLC/account/firehose assertions, but you cannot rotate or rebuild its container without breaking other tests. For tests that need a PDS they fully own (secret rotation, container rebuilds, multi-PDS migration), spawn a disposable one. Its DIDs still register in the shared testnet PLC.

```
use SocialDept\AtpTestnet\Data\PdsSpec;

$spawned = $testnet->spawnPds(new PdsSpec(name: 'my-pds', hostPort: 7300));

$pds = $spawned->pds();
$account = $pds->createAccount($pds->handle('alice')); // alice.my-pds.test
$doc = $testnet->plc()->getDocument($account->did);     // resolves on the shared PLC

$testnet->despawnPds($spawned); // or $testnet->stop() tears down all spawned PDSes
```

Only `name` + `hostPort` are required; secrets are generated when omitted so the PDS is isolated. Supply `dataPath` for a host bind-mount (survives container rebuilds), `network` to attach to `$testnet->networkName()`, or explicit secrets/`hostname` so a consumer that rebuilds the container itself can reproduce its exact configuration.

To instead run a PDS container entirely yourself against the shared testnet, point its `PDS_DID_PLC_URL` at `$testnet->plcUrlForContainers()` and (optionally) `$testnet->requestRelayCrawl($yourPdsHostname)` to have the relay index it.

API Reference
-------------

[](#api-reference)

### Testnet

[](#testnet)

```
// Lifecycle
Testnet::start(?TestnetConfig $config = null): Testnet
$testnet->stop(): void
$testnet->isRunning(): bool

// Accounts
$testnet->createAccount(string $handle, ?string $email = null): TestAccount
$testnet->createAccountWithSession(string $handle, ?string $email = null): TestAccount
$testnet->authenticatedClient(TestAccount $account): \GuzzleHttp\Client

// Services
$testnet->plc(): PlcService
$testnet->pds(): PdsService
$testnet->relay(): RelayService

// Relay
$testnet->requestRelayCrawl(?string $pdsHostname = null): void  // null = built-in testnet PDS

// Bring-your-own / disposable PDS (shares the testnet PLC + relay)
$testnet->plcUrlForContainers(): string   // PLC URL reachable from a container you run yourself
$testnet->networkName(): string           // compose network to attach an external container to
$testnet->spawnPds(PdsSpec $spec): SpawnedPds
$testnet->despawnPds(SpawnedPds|string $pds): void

// Reset (for test isolation)
$testnet->resetPds(): void       // Truncate all PDS accounts and repos
$testnet->resetPlc(): void       // Truncate all DIDs and operations
$testnet->resetRelay(): void     // Truncate relay subscriptions and data
$testnet->resetAll(): void       // Reset PDS + PLC + Relay

// Config
$testnet->config(): TestnetConfig
$testnet->rotationKeypair(): Secp256k1Keypair
```

### PLC Service

[](#plc-service)

```
// DID documents
$testnet->plc()->getDocument(string $did): array
$testnet->plc()->getDocumentData(string $did): array
$testnet->plc()->getLastOperation(string $did): array
$testnet->plc()->getOperationLog(string $did): array
$testnet->plc()->getAuditLog(string $did): array
$testnet->plc()->submitOperation(string $did, array $operation): void

// PLC operations (requires atp-cbor for signing)
$testnet->plc()->updateServiceEndpoint(string $did, string $newEndpoint, Secp256k1Keypair $signer): void
$testnet->plc()->updateHandle(string $did, string $newHandle, Secp256k1Keypair $signer): void
$testnet->plc()->updateRotationKeys(string $did, array $newRotationKeys, Secp256k1Keypair $signer): void

$testnet->plc()->isHealthy(): bool
```

### PDS Service

[](#pds-service)

```
// Handles — "local.{service handle domain}"
$testnet->pds()->handle(string $local): string   // built-in PDS: "local.test"
$spawned->pds()->handle(string $local): string   // spawned PDS: "local.{its hostname}"

// Accounts
$testnet->pds()->createAccount(string $handle, ?string $email = null, ?string $password = null): TestAccount
$testnet->pds()->deleteAccount(string $did, string $password, string $token): array
$testnet->pds()->deactivateAccount(string $accessJwt): array
$testnet->pds()->activateAccount(string $accessJwt): array

// Sessions
$testnet->pds()->createSession(string $identifier, string $password): array
$testnet->pds()->getSession(string $accessJwt): array
$testnet->pds()->refreshSession(string $refreshJwt): array
$testnet->pds()->deleteSession(string $refreshJwt): void

// Records
$testnet->pds()->createRecord(string $collection, array $record, string $accessJwt, ?string $repo = null): array
$testnet->pds()->getRecord(string $repo, string $collection, string $rkey): array
$testnet->pds()->deleteRecord(string $collection, string $rkey, string $accessJwt, ?string $repo = null): array
$testnet->pds()->listRecords(string $repo, string $collection, int $limit = 50): array

// Repos and blobs
$testnet->pds()->describeRepo(string $repo): array
$testnet->pds()->uploadBlob(string $data, string $mimeType, string $accessJwt): array
$testnet->pds()->getRepo(string $did): string

// Identity
$testnet->pds()->resolveHandle(string $handle): array
$testnet->pds()->updateHandle(string $handle, string $accessJwt): array

// Admin
$testnet->pds()->createInviteCode(int $useCount = 1, ?string $forAccount = null): array
$testnet->pds()->getAccountInfo(string $did): array
$testnet->pds()->adminUpdateHandle(string $did, string $handle): array
$testnet->pds()->adminUpdateEmail(string $did, string $email): array
$testnet->pds()->updateSubjectStatus(string $did, string $deactivated = 'false'): array

$testnet->pds()->describeServer(): array
$testnet->pds()->isHealthy(): bool
```

### Relay Service

[](#relay-service)

```
$testnet->relay()->requestCrawl(string $pdsHostname): void
$testnet->relay()->listRepos(?int $limit = null, ?string $cursor = null): array
$testnet->relay()->listHosts(?int $limit = null): array
$testnet->relay()->getRepoStatus(string $did): array
$testnet->relay()->getLatestCommit(string $did): array
$testnet->relay()->getBlob(string $did, string $cid): string
$testnet->relay()->consumeFirehose(int $timeoutMs = 3000, ?int $cursor = null): array

$testnet->relay()->isHealthy(): bool
```

### TestAccount

[](#testaccount)

```
$account->did;        // did:plc:abc123...
$account->handle;     // alice.test
$account->email;      // alice@test.invalid
$account->password;   // auto-generated
$account->accessJwt;  // session token
$account->refreshJwt; // refresh token
```

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance95

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

5

Last Release

22d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/101910626?v=4)[Social Dept.](/maintainers/socialdept)[@socialdept](https://github.com/socialdept)

---

Top Contributors

[![btrsco](https://avatars.githubusercontent.com/u/1373528?v=4)](https://github.com/btrsco "btrsco (9 commits)")

---

Tags

testingdockerRelayintegration-testingat protocolplcatpTestnetPDS

###  Code Quality

TestsPHPUnit

Code StylePHP CS Fixer

### Embed Badge

![Health badge](/badges/socialdept-atp-testnet/health.svg)

```
[![Health](https://phpackages.com/badges/socialdept-atp-testnet/health.svg)](https://phpackages.com/packages/socialdept-atp-testnet)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.7k532.1M19.2k](/packages/laravel-framework)[brianium/paratest

Parallel testing for PHP

2.5k129.9M905](/packages/brianium-paratest)[tempest/framework

The PHP framework that gets out of your way.

2.2k31.1k11](/packages/tempest-framework)[orchestra/testbench

Laravel Testing Helper for Packages Development

2.2k41.3M38.3k](/packages/orchestra-testbench)[laravel/dusk

Laravel Dusk provides simple end-to-end testing and browser automation.

1.9k38.6M289](/packages/laravel-dusk)[infection/infection

Infection is a Mutation Testing framework for PHP. The mutation adequacy score can be used to measure the effectiveness of a test set in terms of its ability to detect faults.

2.2k27.9M2.2k](/packages/infection-infection)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
