Laravel Satis Development
When to use this skill
Use this skill when:
- Creating or managing credentials for private repositories
- Creating or managing private Composer packages and tokens
- Working with Satis builds and repository configuration
- Implementing token-based authentication for package access
- Configuring multi-tenancy for package isolation
- Handling GitHub webhooks for automatic rebuilds
- Tracking package dependencies and downloads
- Extending or customizing models, jobs, or actions
Database Schema
Tables (prefix: satis_)
| Table | Purpose |
|---|---|
satis_credentials |
Authentication credentials (url, email, password) |
satis_packages |
Registered packages (Composer/GitHub) with credential_id FK |
satis_tokens |
Authentication tokens |
satis_package_token |
Package-Token pivot |
satis_package_releases |
Package versions from Satis builds |
satis_dependencies |
Package dependencies (public/private) |
satis_dependency_package_release |
Dependency-Release pivot (with version) |
satis_package_downloads |
Download statistics per version |
satis_packagists |
Public package lookup cache |
Model Usage
Always use ModelResolver to reference models — never hardcode class names:
use JeffersonGoncalves\LaravelSatis\Support\ModelResolver;
// Correct
$credential = ModelResolver::credential()::create([...]);
$package = ModelResolver::package()::create([...]);
$token = ModelResolver::token()::find($id);
// Incorrect - don't do this
$package = new Package();
Creating a Credential
use JeffersonGoncalves\LaravelSatis\Support\ModelResolver;
$credential = ModelResolver::credential()::create([
'name' => 'My Private Repo',
'url' => 'https://repo.example.com',
'email' => 'user',
'password' => 'secret',
]);
// is_validated defaults to false, validated_at defaults to null
Validating a Credential
use JeffersonGoncalves\LaravelSatis\Actions\ValidateCredential;
$result = app(ValidateCredential::class)->execute($credential);
// Returns: ['success' => bool, 'message' => string]
// Updates is_validated and validated_at on the credential
Creating a Package
use JeffersonGoncalves\LaravelSatis\Enums\PackageType;
use JeffersonGoncalves\LaravelSatis\Support\ModelResolver;
$package = ModelResolver::package()::create([
'name' => 'vendor/package-name',
'type' => PackageType::Composer,
'credential_id' => $credential->id, // required FK
]);
// webhook_secret (64 chars) and reference (32 chars) are auto-generated by PackageObserver
Creating a Token
$token = ModelResolver::token()::create([
'name' => 'CI/CD Token',
'email' => 'dev@example.com',
]);
// token value (64-char) is auto-generated by TokenObserver
// SyncTokenPackages job is auto-dispatched on creation
// Associate packages with token
$token->packages()->attach([$package->id]);
Package Types
use JeffersonGoncalves\LaravelSatis\Enums\PackageType;
PackageType::Composer // Composer repository with packages.json
PackageType::Github // GitHub VCS repository
Dependency Types
use JeffersonGoncalves\LaravelSatis\Enums\DependencyType;
DependencyType::Public // Exists on Packagist
DependencyType::Private // Custom/private package
DTOs
PackageData
use JeffersonGoncalves\LaravelSatis\Data\PackageData;
new PackageData(name: 'vendor/pkg', version: '*');
RepositoryData
use JeffersonGoncalves\LaravelSatis\Data\RepositoryData;
new RepositoryData(name: 'vendor/pkg', type: 'composer', url: 'https://repo.example.com');
Jobs
Build Pipeline
satis:build command
└─> SyncTenantPackages (timeout: 86400s)
├─> Groups packages by credential_id
├─> For each credential group:
│ ├─> Builds satis.json with inline auth URLs (RFC 3986)
│ ├─> Writes auth.json for Composer home
│ ├─> Runs `php satis build` with retry (max 3, backoff on 429)
│ └─> Saves snapshot of packages.json
├─> Merges all snapshots via MergeSatisPackagesJson
├─> Sanitizes output (removes inline credentials)
└─> SyncTokenPackages (per token, same grouping/retry logic)
Dispatching Builds Manually
use JeffersonGoncalves\LaravelSatis\Jobs\SyncTenantPackages;
use JeffersonGoncalves\LaravelSatis\Jobs\SyncTokenPackages;
// Build for specific tenant
SyncTenantPackages::dispatch($tenantId);
// Build for specific token
SyncTokenPackages::dispatch($token);
Actions
Validate Credential
use JeffersonGoncalves\LaravelSatis\Actions\ValidateCredential;
$result = app(ValidateCredential::class)->execute($credential);
// ['success' => true, 'message' => 'Credential validated successfully.']
Validate Package Credentials
use JeffersonGoncalves\LaravelSatis\Actions\ValidatePackageCredentials;
$action = new ValidatePackageCredentials();
$isValid = $action->execute($package); // Returns bool, uses $package->credential
Process Package Dependencies
use JeffersonGoncalves\LaravelSatis\Actions\ProcessPackageDependency;
$action = new ProcessPackageDependency();
$action->execute($packageRelease, $requireArray);
Merge Satis Snapshots
use JeffersonGoncalves\LaravelSatis\Actions\MergeSatisPackagesJson;
$action = new MergeSatisPackagesJson();
$action->handle($outputDir, $snapshotPaths, $disk);
// Merges packages, available-packages, includes from multiple snapshots
Sanitize Satis Output
use JeffersonGoncalves\LaravelSatis\Actions\SanitizeSatisPackages;
$sanitizer = new SanitizeSatisPackages();
$sanitizer->sanitizeDirectory($buildPath, $disk);
// Removes transport-options and inline credentials from JSON files
Satis Configuration
use JeffersonGoncalves\LaravelSatis\Support\SatisConfig;
use JeffersonGoncalves\LaravelSatis\Data\PackageData;
use JeffersonGoncalves\LaravelSatis\Data\RepositoryData;
// Fluent builder API
$config = SatisConfig::make()
->homepage('https://satis.example.com')
->notifyBatch(route('composer.downloads'))
->httpBasic('repo.example.com', 'user', 'pass')
->repository(new RepositoryData(name: 'vendor/pkg', type: 'composer', url: 'https://repo.example.com'))
->require(new PackageData(name: 'vendor/pkg'));
$json = $config->toJson();
echo (string) $config; // Implements Stringable
// Or use setPackages() for automatic credential handling
$config = SatisConfig::make()
->setPackages($packages); // Handles conflict detection, deduplication, auth headers
Authentication Flow
The EnsureUserHasLicense middleware authenticates requests in this order:
- HTTP Basic Auth — token value as password
- Bearer Token —
Authorization: Bearer {token}
# Composer usage in composer.json
composer config repositories.private composer https://example.com/satis
composer config http-basic.example.com "" "TOKEN_VALUE_HERE"
Multi-Tenancy
Enable in config:
// config/satis.php
'tenancy' => [
'enabled' => true,
'model' => \App\Models\Tenant::class,
'foreign_key' => 'tenant_id',
'ownership_relationship' => null,
'resolver' => fn () => auth()->user()?->tenant_id,
],
The HasTenancy trait on models (Credential, Package, Token) automatically:
- Adds global scope filtering by tenant
- Sets
tenant_idon model creation - Provides
tenant()BelongsTo relationship
Routes with tenancy: {tenant}/satis/packages.json
Observers & Auto-Generation
| Model | Event | Auto-Action |
|---|---|---|
| Package | creating | Generates webhook_secret (64 chars), reference (32 chars) |
| Package | created | Dispatches ValidatePackageCredentialsJob |
| Package | updated | Resets credential validation if credential_id changed |
| Package | updated | Dispatches SyncTokenPackages when credentials become validated |
| Token | creating | Generates 64-char token |
| Token | created | Dispatches SyncTokenPackages |
| Token | deleted | Deletes storage directory |
| Dependency | creating | Auto-determines type via Packagist lookup |
GitHub Webhooks
Packages auto-generate a reference (32-char) for webhook URLs:
POST /api/satis/webhooks/github/{package.reference}
The webhook validates HMAC-SHA256 signature using webhook_secret (64-char) and dispatches a rebuild.
Extending Models
Override models in config:
// config/satis.php
'models' => [
'credential' => \App\Models\CustomCredential::class,
'package' => \App\Models\CustomPackage::class,
// ...
],
Custom models must implement the corresponding contract interface (e.g., CredentialContract, PackageContract).
Testing
The package uses Pest with Orchestra Testbench and SQLite in-memory database:
use JeffersonGoncalves\LaravelSatis\Tests\TestCase;
uses(TestCase::class)->in('Feature', 'Unit');
Run tests:
php vendor/bin/pest
Converted and distributed by TomeVault — claim your Tome and manage your conversions.