PHPackages                             intrfce/laravel-prefixed-uuids - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. intrfce/laravel-prefixed-uuids

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

intrfce/laravel-prefixed-uuids
==============================

Stripe-style, reversible prefixed public IDs for Eloquent models, backed by UUID v7.

v0.2.0(1w ago)01↑2900%MITPHP ^8.3

Since Jul 14Compare

[ Source](https://github.com/intrfce/laravel-public-ids)[ Packagist](https://packagist.org/packages/intrfce/laravel-prefixed-uuids)[ RSS](/packages/intrfce-laravel-prefixed-uuids/feed)WikiDiscussions Synced today

READMEChangelog (2)Dependencies (5)Versions (5)Used By (0)

> Note: This is not production ready, and the docs aren't ready yet.

Laravel Prefixed UUIDs
======================

[](#laravel-prefixed-uuids)

Stripe-style public identifiers for Eloquent models — `user_3kQ4mZp8Vh7kQp2Rt5Nx9`, `cus_0192f8a1...` — that are a **reversible base62 encoding** of the model's **UUID v7** primary key.

```
$user->id;         // "0192f8a1-9b2c-71d4-a716-446655440000"  (raw UUID — the ORM key)
$user->public_id;  // "user_3kQ4mZp8Vh7kQp2Rt5Nx9"            (what the world sees)

User::find('user_3kQ4mZp8Vh7kQp2Rt5Nx9');   // decodes, then finds
route('users.show', $user);                  // /users/user_3kQ4mZp8Vh7kQp2Rt5Nx9
```

Unlike a real Stripe ID (an opaque random token), this ID **decodes back to the UUID with pure math — no database lookup**. That makes it fast and stateless, at the cost of not being opaque (see [Caveats](#caveats)).

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

[](#requirements)

- PHP 8.3+
- Laravel 12+

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

[](#installation)

```
composer require intrfce/laravel-prefixed-uuids
```

The service provider is auto-discovered.

Setup
-----

[](#setup)

### 1. Add the trait and declare a prefix

[](#1-add-the-trait-and-declare-a-prefix)

Each model owns its prefix, returned from an `idPrefix()` method on the class — there is no central registry to keep in sync:

```
use Illuminate\Database\Eloquent\Model;
use Intrfce\PrefixedUuids\Concerns\HasPrefixedUuids;

class User extends Model
{
    use HasPrefixedUuids;

    public function idPrefix(): string
    {
        return 'user';
    }
}
```

The trait builds on Laravel's own `HasUuids`, so the key is a non-incrementing string auto-populated with a UUID v7 on create (override `newUniqueId()` to change that). `idPrefix()` is `abstract` — a model that uses the trait without implementing it won't compile, so there is no runtime "missing prefix" state to guard against.

### 2. Use a UUID primary key in your migration

[](#2-use-a-uuid-primary-key-in-your-migration)

```
Schema::create('users', function (Blueprint $table) {
    $table->uuid('id')->primary();
    // ...
});
```

Usage
-----

[](#usage)

### Reading the ID

[](#reading-the-id)

```
$user = User::create(['name' => 'Ada']);

$user->id;         // raw UUID — used by relationships, foreign keys, joins
$user->getKey();   // same raw UUID
$user->public_id;  // "user_3kQ4mZp8Vh7kQp2Rt5Nx9"
```

The **raw UUID stays the ORM key** — relationships, eager loading, and foreign keys are never touched, so everything keeps working normally.

### JSON / API output

[](#json--api-output)

`toArray()` and `toJson()` present the Public ID as `id` and **hide the raw UUID**:

```
$user->toArray();
// [ "id" => "user_3kQ4mZp8Vh7kQp2Rt5Nx9", "name" => "Ada", ... ]
```

### Route model binding

[](#route-model-binding)

URLs use the Public ID automatically, and implicit binding decodes it:

```
Route::get('/users/{user}', fn (User $user) => $user);

route('users.show', $user);        // /users/user_3kQ4mZp8Vh7kQp2Rt5Nx9
// GET /users/user_3kQ4mZp...       -> resolved
// GET /users/     -> 404 (never a 500)
```

### Querying

[](#querying)

`find()`, `findOrFail()`, `findMany()`, `destroy()`, and `whereIn('id', …)` all accept **either** a bare UUID or a Public ID:

```
User::find('user_3kQ4mZp8Vh7kQp2Rt5Nx9');   // decodes then queries
User::find($rawUuid);                         // still works
User::findMany(['user_3kQ…', 'user_9x…']);
User::destroy('user_3kQ4mZp8Vh7kQp2Rt5Nx9');
User::whereIn('id', [$publicIdA, $publicIdB])->get();

User::find('cus_0Vh7kQp2Rt5Nx93k');           // wrong prefix -> PrefixMismatchException
```

### Assigning the key

[](#assigning-the-key)

```
$user->id = 'user_3kQ4mZp8Vh7kQp2Rt5Nx9';    // validates prefix, stores the raw UUID
$user->id = $rawUuid;                          // also fine
$user->id = 'cus_...';                         // PrefixMismatchException
```

> Note the deliberate asymmetry: you may *assign* a Public ID, but reading `$user->id` returns the raw UUID. Read `$user->public_id` for the prefixed form.

Validation
----------

[](#validation)

Laravel's built-in `exists` rule runs a raw query and **cannot** match a Public ID against a UUID column (it silently fails on MySQL and 500s on Postgres). Use the decode-aware rule instead, naming the model directly:

```
use Intrfce\PrefixedUuids\Rules\PublicIdExists;

$request->validate([
    'customer' => PublicIdExists::for(Customer::class)
        ->where('active', true)
        ->withoutTrashed(),
]);
```

Bad input (malformed tail, wrong prefix) fails as a normal validation message — it never throws. The target model must use `HasPrefixedUuids`; its prefix is read from `idPrefix()`.

Exceptions
----------

[](#exceptions)

All extend `Intrfce\PrefixedUuids\Exceptions\PrefixedUuidException`:

ExceptionWhen`PrefixMismatchException`assigning/querying with another model's prefix`InvalidPublicIdException`malformed value or a tail that isn't valid base62How it works
------------

[](#how-it-works)

"The id" is two things that this package deliberately keeps separate:

- **ORM key** — the raw UUID v7. What `getKey()`, foreign keys, joins, and eager loading use. Never changed.
- **Public ID** — `_`, ~22-char tail, URL-safe. Shown on JSON, URLs, and the `public_id` accessor.

Decoding is pure integer arithmetic over the UUID's 16 raw bytes (no `ext-gmp`/`ext-bcmath` required). The full rationale for every decision lives in [`docs/`](./docs/README.md) as ADRs.

Caveats
-------

[](#caveats)

- **Not opaque.** A Public ID reveals its UUID, and because UUID v7 embeds a creation timestamp, it reveals *when the record was created*. If you need unguessable external IDs, this is the wrong tool.
- **A prefix is mandatory.** A model using the trait must implement `idPrefix()` — the method is abstract, so this is enforced at compile time, not at runtime.
- **No global resolver.** Because prefixes live on models (not in a central map), there is no `resolve('cus_…') -> Customer` lookup from a bare Public ID. Operations are model-scoped: `Customer::find('cus_…')`, route binding, `$model->public_id`.
- **set/get asymmetry** on the key is intentional — assign a Public ID, read back a raw UUID.

Testing
-------

[](#testing)

```
composer install
./vendor/bin/pest
```

The suite includes an informational benchmark (never gates the build):

```
./vendor/bin/pest --group=benchmark
# [benchmark] encode 1000: ~12 ms | decode 1000: ~12 ms
```

License
-------

[](#license)

MIT.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance98

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

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 ~1 days

Total

2

Last Release

7d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e103145d2ee7625eacb8d240214a90e4c54b8add29ea4433be0b02416e553974?d=identicon)[danmatthews](/maintainers/danmatthews)

---

Tags

laravelstripeuuididentifiersprefixed iduuid7

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/intrfce-laravel-prefixed-uuids/health.svg)

```
[![Health](https://phpackages.com/badges/intrfce-laravel-prefixed-uuids/health.svg)](https://phpackages.com/packages/intrfce-laravel-prefixed-uuids)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k30.2M151](/packages/laravel-cashier)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M244](/packages/laravel-ai)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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