NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The published config/contacts.php:

return [
    'model' => RoundlyConsulting\Contacts\Models\Contact::class,
    'table' => 'contacts',
    'key_type' => env('CONTACTS_KEY_TYPE', 'bigint'),

    'auto_primary' => true,
    'require_owner_for_primary' => false,
    'default_country_code' => env('CONTACTS_DEFAULT_COUNTRY_CODE'),

    'verification' => [
        'ttl' => env('CONTACTS_VERIFICATION_TTL', 60),
        'style' => env('CONTACTS_VERIFICATION_STYLE', 'code'),
        'code_length' => env('CONTACTS_VERIFICATION_CODE_LENGTH', 6),
        'token_length' => env('CONTACTS_VERIFICATION_TOKEN_LENGTH', 32),
        'max_attempts' => env('CONTACTS_VERIFICATION_MAX_ATTEMPTS', 5),
    ],

    'types' => [
        // 'whatsapp' => ['label' => 'WhatsApp', 'icon' => 'chat', 'rules' => ['required', 'string']],
    ],

    'relationship_kinds' => [
        // 'works_at' => 'Works at', 'spouse_of' => 'Spouse of',
    ],
];

Every key

KeyDefaultEnvPurpose
modelContact::class—Eloquent model for contacts. Must be the package’s Contact or extend it — anything else throws InvalidConfigurationException.
tablecontacts—Table contacts live in; read by the migration and by Contact::getTable(). Blank = not set (contacts); any other value must be a string.
key_typebigintCONTACTS_KEY_TYPEKey type of owner_id: bigint, uuid or ulid. Anything else throws InvalidConfigurationException. Fixed when the migration runs.
auto_primarytrue—The first contact of a kind added for an owner becomes its primary; when the primary leaves a kind (deleted, synced away, moved), the next by position takes over.
require_owner_for_primaryfalse—Only owned contacts may be primary; otherwise PrimaryContactConflict is thrown.
default_country_codenullCONTACTS_DEFAULT_COUNTRY_CODEBest-effort dialling prefix for phone numbers entered in national format — it replaces the trunk 0 (see Contact kinds). Written 421, +421 or 1-264 (1–4 digits, no leading zero); not set (null or blank) means none.
verification.ttl60CONTACTS_VERIFICATION_TTLMinutes a verification token stays valid, 1–525600 (a year).
verification.stylecodeCONTACTS_VERIFICATION_STYLEcode for a numeric one-time code, token for a random hex string. Exact and lower-case: anything else throws, so a typo never downgrades a token to a 6-digit code.
verification.code_length6CONTACTS_VERIFICATION_CODE_LENGTHNumber of digits when style is code, 1–72.
verification.token_length32CONTACTS_VERIFICATION_TOKEN_LENGTHBytes of randomness when style is token, 1–36 — hex-encoded, so the string is twice as long; bcrypt reads only the first 72 characters.
verification.max_attempts5CONTACTS_VERIFICATION_MAX_ATTEMPTSWrong guesses a token survives, 1–1000. The guess that spends the last one voids the token.
types[]—Register custom kinds and override the label, icon or rules of built-in kinds. A kind => definition map; label and icon are strings (blank = not set, so the kind’s own) and rules a list of rule strings.
relationship_kinds[]—Allow-list for relateTo() / relationsOfKind(). Empty or not set (null or blank) = free-form; a list or kind => label map restricts kinds. Any other non-array value throws.

The two switches accept env-style strings ('true'/'false', '1'/'0', 'on'/'off', 'yes'/'no'), and the numeric keys accept integer strings ('30'). A key that is not set (absent, null, or blank: '' or whitespace, as a bare CONTACTS_VERIFICATION_TTL= line gives) takes its default. Anything else — a mistyped switch or style, a TTL, length or attempt count that is not a whole number ('five', '5.5') or is out of range, a non-string table, a country code that isn’t one, a malformed types or relationship_kinds entry — throws the package toolkit’s InvalidConfigurationException naming the key, rather than being read as a default or clamped.

Environment

The deployment-sensitive keys are env-backed, so you rarely need to publish the file:

CONTACTS_KEY_TYPE=bigint
CONTACTS_DEFAULT_COUNTRY_CODE=421
CONTACTS_VERIFICATION_TTL=60
CONTACTS_VERIFICATION_STYLE=code
CONTACTS_VERIFICATION_CODE_LENGTH=6
CONTACTS_VERIFICATION_TOKEN_LENGTH=32
CONTACTS_VERIFICATION_MAX_ATTEMPTS=5

Custom contact model

The Contact model is intentionally not final. Extend it and point contacts.model at your class — the trait, actions, vCard exporter, notification routing and test matchers all resolve it from there:

namespace App\Models;

use RoundlyConsulting\Contacts\Models\Contact as BaseContact;

class Contact extends BaseContact
{
    // your own relations, accessors and scopes
}

// config/contacts.php
'model' => App\Models\Contact::class,

The configured class must be the package’s Contact or a subclass of it; anything else — a foreign model, a class that doesn’t exist — throws InvalidConfigurationException naming contacts.model instead of being silently replaced. Change the table through contacts.table rather than a $table property — the model’s getTable() honours the config value.

Inspecting the configuration

The package reports its configuration to Laravel’s about command:

php artisan about --only=contacts
LineShows
ModelClass basename of the resolved contact model.
TableThe configured table name.
Auto primaryON / OFF.
Require owner for primaryON / OFF.
Default country codeSET / NONE — never the prefix itself.
VerificationStyle, TTL and attempt limit, e.g. code, 60m, 5 attempts.
Custom typesA count, e.g. 2 registered — never the kind names.
Relationship kindsFREE-FORM, or a count such as 3 allowed.

Registered kinds and relationship kinds are your domain vocabulary, so they are reported as counts only — never by name. The country code is reported by presence, never its value. A malformed table, country code, verification setting, types or relationship_kinds value shows as INVALID instead of failing the command.

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 crypto

By 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.