# Laravel Satis Development

> Build and work with Laravel Satis private Composer repository features, including credentials, packages, tokens, authentication, multi-tenancy, and Satis builds. Use when this capability is needed.

- Skill: `tomevault-io/laravel-satis-development` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/laravel-satis-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/laravel-satis-development/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/laravel-satis-development

---


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

```php
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

```php
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

```php
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

```php
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

```php
$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

```php
use JeffersonGoncalves\LaravelSatis\Enums\PackageType;

PackageType::Composer  // Composer repository with packages.json
PackageType::Github    // GitHub VCS repository
```

### Dependency Types

```php
use JeffersonGoncalves\LaravelSatis\Enums\DependencyType;

DependencyType::Public   // Exists on Packagist
DependencyType::Private  // Custom/private package
```

## DTOs

### PackageData

```php
use JeffersonGoncalves\LaravelSatis\Data\PackageData;

new PackageData(name: 'vendor/pkg', version: '*');
```

### RepositoryData

```php
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

```php
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

```php
use JeffersonGoncalves\LaravelSatis\Actions\ValidateCredential;

$result = app(ValidateCredential::class)->execute($credential);
// ['success' => true, 'message' => 'Credential validated successfully.']
```

### Validate Package Credentials

```php
use JeffersonGoncalves\LaravelSatis\Actions\ValidatePackageCredentials;

$action = new ValidatePackageCredentials();
$isValid = $action->execute($package); // Returns bool, uses $package->credential
```

### Process Package Dependencies

```php
use JeffersonGoncalves\LaravelSatis\Actions\ProcessPackageDependency;

$action = new ProcessPackageDependency();
$action->execute($packageRelease, $requireArray);
```

### Merge Satis Snapshots

```php
use JeffersonGoncalves\LaravelSatis\Actions\MergeSatisPackagesJson;

$action = new MergeSatisPackagesJson();
$action->handle($outputDir, $snapshotPaths, $disk);
// Merges packages, available-packages, includes from multiple snapshots
```

### Sanitize Satis Output

```php
use JeffersonGoncalves\LaravelSatis\Actions\SanitizeSatisPackages;

$sanitizer = new SanitizeSatisPackages();
$sanitizer->sanitizeDirectory($buildPath, $disk);
// Removes transport-options and inline credentials from JSON files
```

## Satis Configuration

```php
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:

1. HTTP Basic Auth — token value as password
2. Bearer Token — `Authorization: Bearer {token}`

```bash
# 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:

```php
// 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_id` on 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:

```php
// 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:

```php
use JeffersonGoncalves\LaravelSatis\Tests\TestCase;

uses(TestCase::class)->in('Feature', 'Unit');
```

Run tests:

```bash
php vendor/bin/pest
```

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/jeffersongoncalves) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-13 -->

