Key types & schema
One table, credits, holds every ledger row for every owner. Its shape:
| Column | Type | Notes |
|---|---|---|
id | bigint / uuid / ulid | Per primary_key_type — auto-incrementing bigint by default. |
creditable_type | string | The owning model’s morph class. |
creditable_id | bigint / uuid / ulid | Per key_type. Indexed together with creditable_type. |
bucket | string | Default 'default', indexed. |
amount | bigInteger | Signed: positive grants, negative deducts. Cast to int. |
description | string, nullable | The optional human-readable note. |
meta | json(b), nullable | Arbitrary structured context, cast to array. |
created_at / updated_at | timestamps | created_at drives point-in-time balances. |
deleted_at | soft deletes | Soft-deleted rows no longer count toward any balance. |
A composite index on (creditable_type, creditable_id, bucket) backs every balance read.
The credit_locks table
The same migration creates credit_locks — one small row per owner that every change bumps, so racing changes of one owner serialise. It holds a version counter only; the ledger stays the single source of truth:
| Column | Type | Notes |
|---|---|---|
creditable_type | string | The owner’s morph class. Primary key together with creditable_id. |
creditable_id | bigint / uuid / ulid | Per key_type — the same type as in credits. |
version | unsignedBigInteger | Default 0. Bumped by every change; never read for a balance. |
Two independent key types
Two settings type two different columns. They are independent — uuid users holding credits under a bigint credits id is perfectly ordinary:
| Setting | Types | Choose |
|---|---|---|
key_type | creditable_id — the morph column pointing at your models, in credits and credit_locks | Your creditable models’ primary key: bigint (default), uuid (HasUuids) or ulid (HasUlids). |
primary_key_type | id — the credits table’s own key | What other packages’ polymorphic columns point at. Keep bigint unless every morph target in your app shares one key type. |
Both are fixed when the migration first runs, so set them before you publish and migrate:
# Decide before publishing and running the migration — both are fixed at migrate time.
CREDITS_KEY_TYPE=uuid # your creditable models use HasUuids
CREDITS_PRIMARY_KEY_TYPE=bigint # the credits table's own id — keep the defaultMatching key_type to your models
key_type must match your creditable models’ primary key: keep the default bigint for auto-incrementing ids, and set uuid or ulid when those models use HasUuids or HasUlids:
CREDITS_KEY_TYPE=uuidLeave it at bigint with uuid-keyed users and PostgreSQL rejects the first change:
SQLSTATE[22P02]: invalid input syntax for type bigint: "01a0e9ca-cf2c-7..."Every creditable model in the application must share that one key type.
Why primary_key_type defaults to bigint
A Credit is something other packages point at polymorphically, and a Laravel morph column ($table->morphs('subject')) is an unsigned bigint. On a strict engine such as PostgreSQL, a uuid credit id will not go into one:
SQLSTATE[22P02]: invalid input syntax for type bigint: "019f6f33-22b8-737f-a581-849e7cdc517a"SQLite will not warn you about this — its type affinity silently stores the string in an integer column, so a green SQLite suite proves nothing here.
The constraint: this assumes every morph target in your application shares one key type. If you set CREDITS_PRIMARY_KEY_TYPE=uuid, the models on the other end of your polymorphic relations need to be uuid-keyed too, and the packages owning those columns need to agree. A mixed application — a uuid Credit and a bigint Comment pointed at by the same morph column — is not supported by this package, by Laravel’s own morphs()/uuidMorphs() split, or by anything else. Pick one key type per application.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.