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

Schema & custom models

Two tables back the package. Their names come from table_names; column names are fixed.

appointments

ColumnTypeNotes
idbigintAuto-incrementing primary key.
uuiduuid, uniqueRandom identity filled on insert — not the key. The ICS UID is built from it.
namestringThe appointment title.
descriptiontext, nullableFree-form description.
statusstring, default pendingCast to the Status enum.
metajsonb, nullableCast to a Collection.
timezonestring, nullableThe zone the appointment was booked in; drives local display.
locationstring, nullableVenue name.
latitude / longitudedecimal(10,7), nullableVenue coordinates; indexed together, exposed as coordinates.
starts_attimestampAlways UTC, whatever app.timezone is. Indexed, and together with ends_at.
ends_attimestamp, nullableAlways UTC. Derived from the duration when not given.
duration_minutesunsigned integer, nullableBack-filled from ends_at when not given.
recurrence_groupuuid, nullableShared by every occurrence of a series; indexed.
created_at / updated_at / deleted_attimestampsSoft deletes.

appointment_participants

ColumnTypeNotes
idbigintAuto-incrementing primary key.
appointment_idforeignIdThe appointment; a plain column without a database-level constraint.
participant_type / participant_idmorphsThe participating model; participant_id is typed by key_type.
rolestring, nullableCast to the ParticipantRole enum.
metajsonb, nullableCast to a Collection.
created_at / updated_at / deleted_attimestampsSoft deletes. Unique index on (appointment_id, participant_type, participant_id).

Each model can join an appointment once — the unique index covers (appointment_id, participant_type, participant_id). Soft-deleted participant rows still occupy that index, so adding a removed participant again restores its old row with the new role and meta (ParticipantUpdated) instead of inserting a second one.

Participant key type

key_type types the participant_id morph column: bigint for Laravel’s default auto-incrementing keys, uuid for models using HasUuids, ulid for models using HasUlids. The value is case-insensitive; any other value throws InvalidConfigurationException when the migrations run, so a typo never builds the wrong column. Every model you attach as a participant must share that key type; the package’s own tables always keep auto-incrementing ids. Set it before you publish and run the migrations:

APPOINTMENTS_KEY_TYPE=uuid
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Foundation\Auth\User as Authenticatable;
use RoundlyConsulting\Appointments\Concerns\HasAppointments;

class User extends Authenticatable
{
    use HasAppointments;
    use HasUuids; // pairs with APPOINTMENTS_KEY_TYPE=uuid
}

Custom models

Point appointments.model and appointments.participant at your own subclasses to add relations, accessors or scopes. Extend the packaged models — they are deliberately not final and bring the location, contacts, reviews and approval traits with them:

namespace App\Models;

use RoundlyConsulting\Appointments\Models\Appointment;
use RoundlyConsulting\Appointments\Models\Participant;

class Booking extends Appointment
{
    // Reads its table from appointments.table_names.appointments — nothing to set.

    public function isToday(): bool
    {
        return $this->startsAtLocal()->isToday();
    }
}

class BookingParticipant extends Participant
{
    // Reads its table from appointments.table_names.participants — nothing to set.
}
// config/appointments.php
'model' => App\Models\Booking::class,
'participant' => App\Models\BookingParticipant::class,
  • Both models read their table from table_names — Appointment from table_names.appointments, Participant from table_names.participants — so a subclass with another class name needs no $table.
  • An unset key (absent, null or blank) resolves the packaged model. Anything else must be the packaged model or a subclass of it — a class that doesn’t extend it throws InvalidConfigurationException naming the key instead of being silently replaced.
  • The participants relation names its foreign key (appointment_id) explicitly, so a swapped model never looks for a column derived from its own class name.
  • The actions, the HasAppointments trait, the availability checks and the participant relation resolve the model from config, so your subclass is what they create and return.

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.