Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Snipe It Laravel Package

snipe/snipe-it

Snipe-IT is a free, open-source IT asset management system built on Laravel. Track hardware, software licenses, accessories, and consumables; manage checkouts, assignments, audits, and reporting through a self-hosted web UI and REST API. Built for IT teams needing lifecycle visibility and accountabi...

View on GitHub
Deep Wiki
Context7

Getting Started

First, reset expectations: this is not a Composer package you require into an existing Laravel app. Snipe-IT is a complete Laravel application (asset/license/consumable tracking with checkout workflows, depreciation, auditing, labels, LDAP/SAML). The Dependents: 0 figure is accurate and expected — there is no library surface to import. You "leverage" it in exactly two ways:

  1. Run it as a system of record and integrate with it over its REST API.
  2. Fork it and extend in place, following its existing conventions.

For 90% of teams, option 1 is the right call. Option 2 drags you into AGPL obligations and upstream merge conflicts (see Gotchas).

Platform baseline (as of v8.8.3)

PHP 8.2.0 is the hard floor; 8.4+ is what upstream recommends and what they test against. v8.8.3 specifically clears PHP 8.4/8.5 deprecation notices, so running 8.4 no longer spams storage/logs/laravel.log with deprecations on every request. If your fleet is still on 8.1 or has 8.2 pinned to an EOL image, you cannot take this line — plan the PHP upgrade before you plan the Snipe-IT upgrade. Upstream has signalled future features will require 8.3+, so treat 8.3 as the realistic near-term target and 8.4 as where you want to be.

Minimal install (Docker, ~5 minutes)

# docker-compose.yml
services:
  app:
    image: snipe/snipe-it:v8.8.3@sha256:<digest>   # pin tag AND digest — see Gotchas
    ports: ["8080:80"]
    env_file: .env
    volumes:
      - snipeit-config:/config
      - snipeit-uploads:/var/lib/snipe-it/public/uploads
    depends_on: [db]

  db:
    image: mariadb:11
    environment:
      MARIADB_DATABASE: snipeit
      MARIADB_USER: snipeit
      MARIADB_PASSWORD: change-me
      MARIADB_ROOT_PASSWORD: change-me-too
    volumes:
      - snipeit-db:/var/lib/mysql

volumes:
  snipeit-config: {}
  snipeit-uploads: {}
  snipeit-db: {}

Set the non-negotiables in .env: APP_URL (must match the real external URL — it drives API URLs, email links, SAML ACS), APP_KEY, APP_ENV=production, APP_DEBUG=false, APP_TIMEZONE, and the DB_* block matching the service above. Then browse to /setup, create the admin, and you're in.

Both the snipe/snipe-it (Debian/Ubuntu) and Alpine image variants are published per release. v8.8.2's Alpine tags were rebuilt as v8.8.3 within minutes of release to pick up Alpine's patched PHP — if you grabbed :latest or :8.8.2 during that window, verify which bits you actually have.

The bare-metal path (git clone, composer install --no-dev, php artisan key:generate, php artisan migrate --seed) works but you inherit PHP extension, permissions, and upgrade chores. Prefer Docker or a maintained image.

Where to look first in the source

Path Why you care
routes/api.php The entire public contract. Read this before writing any integration.
app/Http/Controllers/Api/ Endpoint behaviour, validation, edge cases.
app/Http/Transformers/ Fractal transformers defining exact JSON response shapes.
app/Models/ Asset, AssetModel, User, Location, Statuslabel, License, LicenseSeat, Consumable, Accessory, Component, Actionlog, Setting.
app/Models/Traits/Checkoutable.php The checkout/checkin/audit state machine — do not reimplement it.
app/Console/Commands/ Everything in the snipeit:* namespace (`php artisan list
composer.json require.php is the authoritative version floor for the tag you're on.
.env.example Authoritative list of supported env vars.

First use case: get a token, list hardware

  1. Create a dedicated API user (not your admin account) with the minimum permissions the integration needs.
  2. Log in as that user → avatar menu → API Tokens → create token.
  3. Call it:
curl -s "$SNIPEIT_URL/api/v1/hardware?limit=5&sort=id&order=asc" \
  -H "Authorization: Bearer $SNIPEIT_TOKEN" \
  -H "Accept: application/json"

The response is {"total": 412, "rows": [...]} — note it is not Laravel's default paginator shape.

The highest-value first project is almost always: CSV import of your existing spreadsheet → bulk checkout to users → print asset labels. That validates your data model (models, status labels, locations, custom fields) before you build anything automated on top of it.

Implementation Patterns

Pattern A — Companion Laravel app over the API (recommended)

Keep Snipe-IT as an isolated system of record. Build your reconciliation logic in your app, in your repo, under your licence.

// config/services.php
'snipeit' => [
    'url'   => env('SNIPEIT_URL'),
    'token' => env('SNIPEIT_TOKEN'),
],
namespace App\Services;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Generator;

class SnipeIt
{
    public function assets(array $query = []): array
    {
        return $this->client()->get('api/v1/hardware', $query)->throw()->json();
    }

    /** Stream every asset without loading the whole inventory into memory. */
    public function eachAsset(int $chunk = 500): Generator
    {
        $offset = 0;
        do {
            $page = $this->assets([
                'limit' => $chunk, 'offset' => $offset,
                'sort' => 'id', 'order' => 'asc',
            ]);
            yield from $page['rows'];
            $offset += $chunk;
        } while ($offset < $page['total']);
    }

    public function checkout(int $assetId, int $targetId, string $targetType = 'user'): array
    {
        return $this->client()
            ->post("api/v1/hardware/{$assetId}/checkout", [
                'checkout_to_type' => $targetType,   // user|location|asset
                'assigned_to'      => $targetId,
            ])->throw()->json();
    }

    public function checkin(int $assetId, array $payload = []): array
    {
        return $this->client()
            ->post("api/v1/hardware/{$assetId}/checkin", $payload)->throw()->json();
    }

    protected function client(): PendingRequest
    {
        return Http::baseUrl(config('services.snipeit.url'))
            ->withToken(config('services.snipeit.token'))
            ->acceptJson()
            ->timeout(15)
            ->retry(3, 300, fn ($e) => $e instanceof \Illuminate\Http\Client\RequestException
                && in_array($e->response->status(), [429, 500, 502, 503]), throw: false);
    }
}

Reconciliation job (HR/IdP is the source of truth for people; Snipe-IT for hardware):

public function handle(SnipeIt $api): void
{
    foreach ($api->eachAsset() as $asset) {
        if ($asset['assigned_to'] === null) continue;
        if (! $this->hr->stillEmploys($asset['assigned_to']['id'])) {
            $api->checkin($asset['id'], ['note' => 'Auto-checkin: leaver detected']);
        }
    }
}

Two rules that keep this robust: always use the API for checkout/checkin/audit (never direct DB writes — the API updates status labels, fires events, and writes Actionlog entries), and make the job idempotent by keying off asset_tag/serial, not local row IDs.

Pattern B — Fork and extend in place

Only when you genuinely need behaviour the API can't express (custom report endpoints, bespoke approval flow, new checkout guard rules). Follow the grain of the codebase or upgrades will hurt:

  • Custom fields first. Most "we need to store X" requests are solved in the admin UI: Custom Fields → Fieldsets → attach to Asset Models. Zero code, zero migrations, works through the API. Exhaust this before writing anything.
  • New API endpoint = route in routes/api.php → ApiController subclass → Fractal transformer in app/Http/Transformers/ → FormRequest in app/Http/Requests/.
// routes/api.php
Route::middleware('auth:api')->group(function () {
    Route::get('v1/reports/audit-gaps', [ReportsController::class, 'auditGaps']);
});
  • Notifications go in app/Notifications/ and are wired through config/ + the Settings model — don't hardcode mail logic.
  • Long-running or scheduled work belongs in app/Console/Commands/ and gets registered in the scheduler, mirroring snipeit:expiring-alerts.
  • Keep your PHP version aligned with upstream's composer.json. A fork that pins below the upstream floor turns every merge into a conflict in composer.json/composer.lock.

Pattern C — Read-only reporting

There is no rich audit-log API. If you need "who changed what, when", point a read-only DB user at action_logs (and checkouts) from a separate reporting app or replica. Never write from there.

Data model cheat sheet

Asset ──belongsTo──> AssetModel ──belongsTo──> Manufacturer
  │                       └──belongsToMany──> CustomFieldset ──> CustomField
  ├──belongsTo──> Statuslabel   (meta: deployable|pending|undeployable|archived|lost)
  ├──belongsTo──> Location, Supplier, Company
  ├──morphTo───> assigned_to (User | Location | Asset)
  └──hasMany───> Actionlog, Checkout, AssetMaintenance

License ──hasMany──> LicenseSeat ──benongsTo──> User|Asset
Consumable / Accessory / Component  → independent checkout flows

Deployment as code

  • .env from your secret manager; nothing hand-edited on the box.
  • Cron: * * * * * php /var/www/html/artisan schedule:run — without this, expiring-licence alerts and expected-checkin reminders silently never fire.
  • If you touch QUEUE_CONNECTION, run a worker for snipeit:expiring-alerts and import jobs; the default sync will make big imports time out.
  • Nightly php artisan snipeit:backup (or a DB dump + the uploads volume). Uploads are not in the DB and are the thing everyone forgets.
  • Pin the image by tag + digest, and record the digest alongside the version in your runbook so a re-tagged release is visible in your diff.

Testing a fork

php artisan test requires a separate testing database via .env.testing; the suite leans on transactions and will happily wipe your dev data if you point it at the wrong DB. For companion apps, write contract tests against a disposable Snipe-IT container in CI — that catches upstream API changes on upgrade. Run the same suite on 8.2 and 8.4 if you maintain a fork, since deprecation cleanups land in patch releases.

Gotchas and Tips

AGPL-3.0 is the biggest one. Network-use copyleft: if you modify Snipe-IT and let users interact with it over a network, you must offer them the corresponding source. Internal-only deployments are usually fine; embedding it in a product you ship or host for customers is a legal conversation. The companion-app pattern (Pattern A) keeps your proprietary code in a separate process with a clean boundary — this is a real architectural driver, not just a preference.

PHP 8.2 is a hard floor from this line onward — 8.4+ is the recommended target. This is the change that bites hardest in practice, because the failure mode is a container that won't boot or a composer install that resolves nothing, not a nice error in the UI. Audit your base images and CI runners before upgrading. Upstream has explicitly warned that 8.3+ will be required for forthcoming features, so do not stop at "just enough to boot" — you'll repeat the exercise in two releases.

Releases get re-tagged. v8.8.2 was published and then re-cut as v8.8.3 within minutes because Alpine shipped patched PHP mid-release. Tag names are not immutable in this project. Pin digests in production, keep a changelog of which digest you're running, and always re-pull rather than assuming a tag's contents. If you build a derivative image, rebuild it on every patch release.

Patch releases carry deprecation noise fixes, not just features. v8.8.3's only functional change outside the PHP rebuild is clearing PHP 8.4/8.5 deprecation notices. That matters: deprecation spam in storage/logs/laravel.log masks real errors and can blow up log ingestion costs. If your monitoring is drowning in E_DEPRECATED, take the patch rather than muting the channel.

Settings live in the database, not config/. Setting::getSettings() is cached. After changing anything in Admin → Settings, clear the cache; from CLI, remember the web process may have cached a different view. APP_URL mismatch is the #1 cause of broken email links, wrong API pagination URLs, and SAML failures.

API tokens inherit the user's permissions. A superadmin token in a cron job is a privilege-escalation footgun. Make one integration user per integration, drop it to the narrowest role that works, and rotate tokens on offboarding.

Response shape differs from Laravel's default. Expect {total, rows} with limit/offset/sort/order/search — not data/meta/links. Don't reach for ->collect()->paginate() idioms.

Rate limits are real. Default throttling returns 429; the retry() above handles it, but bulk imports should be queued and chunked, not looped synchronously in a request.

Status labels have meta values (deployable, pending, undeployable, archived, lost) that the checkout logic depends on. Creating custom labels with the wrong meta produces assets that silently refuse to check out.

Never write to assets / checkouts directly. You'll bypass Actionlog, leave assigned_to polymorphic columns inconsistent, and skip status-label transitions. The API exists precisely to keep that invariant.

Uploads and permissions. public/uploads (logos, asset images, avatars) must be writable and persisted outside the container; php artisan storage:link for anything served through storage. Losing that volume on upgrade is a classic incident. Also watch FILE_UPLOAD_MAX_SIZE vs upload_max_filesize/post_max_size.

Database choice. MariaDB/MySQL is the supported path — SQLite is for evaluation only. On older MySQL with utf8mb4, watch index key-length limits during migrations.

Upgrades are migrations, and upgrades are not always reversible. Pin a release tag (and digest), read the release notes (there have been security advisories in older lines — run composer audit on any fork), back up DB and uploads, then use php artisan snipeit:upgrade. Rehearse on a restored copy first; a failed migration mid-upgrade is a bad afternoon. Read the release body, not just the version number — the PHP floor and re-tag notes only ever appear in prose.

Debugging. APP_DEBUG=true locally, storage/logs/laravel.log and LOG_CHANNEL for the rest; php artisan config:clear cache:clear before believing any weird behaviour after an .env edit. php artisan about (shows PHP version and extensions at a glance — your first stop for "does this image meet the floor?"), php artisan route:list --path=api, and composer check-platform-reqs answer most "why won't it boot / why is this 404" questions in seconds.

Extension points worth knowing: model observers (app/Observers) for side effects on checkout, custom Fractal transformers to shape API output for your consumers, app/Notifications for Slack/Teams/webhook-ish delivery, and config/ overrides. There is deliberately no API for creating custom fields — configure them in the UI and treat that configuration as versioned infrastructure (document it, or script it against the DB in a controlled migration in your fork only).

Scheduling is opt-in and silent when absent. If alerts, expected check-ins, or LDAP sync "just don't happen", check cron before you check code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings