Skip to content

Repository files navigation

Event Management

A Laravel 12 REST API for managing events and attendee registrations. Users authenticate with Sanctum bearer tokens, create events (name, description, start/end time), and register as attendees; a console command emails reminders for events starting within the next 24 hours. JSON-only — the sole web page is an API landing page mapping every endpoint.

New developer? Start with .docs/tldr.md — every doc summarised on one page. The full guide lives in .docs/. The complete API reference is openapi.yaml (OpenAPI 3.0 — kept honest by a route-coverage test).

API landing page at / — endpoint table with auth requirements

Prerequisites

Tool Version Installed by
PowerShell + winget Windows 10/11 stock — (the only true prerequisites)
Git any recent setup.ps1
Node.js LTS setup.ps1
PHP 8.4 (repo requires ^8.2) setup.ps1 (zip from php.net into %LOCALAPPDATA%\Programs\php-8.4)
Composer 2.x setup.ps1 (phar + wrapper next to PHP)
uv + Python latest setup.ps1 (runs the .claude tooling)
just any recent setup.ps1
Claude Code CLI latest setup.ps1 (optional, for AI-assisted dev)

Quick start

# 1. One-time machine setup (idempotent — safe to re-run)
pwsh ./setup.ps1

# 2. Close and reopen PowerShell so PATH updates land

# 3. One-time app bootstrap: composer + npm + Vite build + .env + sqlite + migrate
just bootstrap

# 4. Seed demo data — 1000 users / 200 events (takes a couple of minutes)
just fresh

# 5. Start the dev server
just start

The app is now at http://127.0.0.1:8109. Stop it with just stop. Every /api endpoint except POST /api/login requires a Sanctum bearer token — log in as any seeded user (password password), then send the token. See the API quick tour below.

API quick tour

Full endpoint/param/response reference: openapi.yaml. Real request/response pairs against seeded data (just fresh):

1. Log in → token

POST /api/login
Content-Type: application/json
Accept: application/json

{"email": "dolly72@example.com", "password": "password"}
{"token": "1|kJqtI91WstSiRvHndrtQqA5KlzDonnfMwhMIKDiUce8d6e4c"}

Any seeded user works — every seeded password is literally password.

2. List events (paginated, bearer token required)

GET /api/events
Accept: application/json
Authorization: Bearer 1|kJqtI91WstSiRvHndrtQqA5KlzDonnfMwhMIKDiUce8d6e4c
{
  "data": [
    {
      "id": 1,
      "name": "Est consectetur est nihil.",
      "start_time": "2026-08-09 10:12:26",
      "end_time": "2026-09-28 05:35:16",
      "description": "Incidunt consectetur incidunt labore quae et et sit. …",
      "user_id": 119
    }
  ],
  "links": {
    "first": "http://127.0.0.1:8109/api/events?page=1",
    "last": "http://127.0.0.1:8109/api/events?page=14",
    "prev": null,
    "next": "http://127.0.0.1:8109/api/events?page=2"
  },
  "meta": {"current_page": 1, "per_page": 15, "total": 200}
}

(data trimmed to one of 15 items; meta abridged.) Without the token the same request returns 401 {"message":"Unauthenticated."}.

3. Embed relations with ?include=

GET /api/events?include=user
{
  "data": [
    {
      "id": 1,
      "name": "Est consectetur est nihil.",
      "start_time": "2026-08-09 10:12:26",
      "end_time": "2026-09-28 05:35:16",
      "description": "Incidunt consectetur incidunt labore quae et et sit. …",
      "user_id": 119,
      "user": {
        "id": 119,
        "name": "Ola Rosenbaum",
        "email": "aurore17@example.com",
        "email_verified_at": "2026-08-02T02:30:53.000000Z",
        "created_at": "2026-08-02T02:30:54.000000Z",
        "updated_at": "2026-08-02T02:30:54.000000Z"
      }
    }
  ]
}

Events accept include=user,attendees; attendee endpoints accept include=user,event. Ownership rules: anyone authenticated can create events and register attendance; update and delete of an event are owner-only (403 for everyone else, enforced by EventPolicy).

Event reminders

php artisan app:send-event-reminders finds every event starting within the next 24 hours and queues an EventReminderNotification (mail) for each attendee. With the default .env (QUEUE_CONNECTION=database, MAIL_MAILER=log) the notifications sit in the jobs table until you run php artisan queue:work --stop-when-empty, and the mail lands in storage/logs/laravel.log.

The command is scheduledroutes/console.php registers it daily (midnight, cron 0 0 * * *):

Schedule::command(SendEventReminders::class)->daily();

A schedule worker still has to be running for it to fire. Locally, activate it with:

just schedule        # php artisan schedule:work — keeps running until Ctrl+C
just schedule-list   # show every scheduled command + next due time

On a server, use a single cron entry instead: * * * * * php artisan schedule:run. Without one of those, the schedule is registered but nothing triggers it.

Commands

Run just with no arguments to list every recipe. The ones you'll use daily:

Command What it does
just bootstrap One-time app setup: deps, .env, sqlite db, migrate, asset build
just start Serve at http://127.0.0.1:8109 in a background window
just serve Serve in the foreground (Ctrl+C to stop)
just stop Stop only THIS repo's php artisan serve
just migrate Run pending migrations
just fresh Drop + re-migrate + seed demo data (IRREVERSIBLE locally)
just schedule Run the scheduler in the foreground (activates the daily reminder job)
just schedule-list List scheduled commands with their cron expression + next due time
just test PHPUnit suite (just test --filter=X passes through)
just lint / just lint-fix Laravel Pint style check / auto-fix
just claudex Launch Claude Code (Sonnet, all permissions)

Testing

just test                          # whole suite
just test --filter=EventApiTest    # one class

PHPUnit 11 via php artisan test. phpunit.xml overrides the database to sqlite :memory:, so tests never touch your seeded database/database.sqlite. What's covered:

Test Guards
tests/Feature/AuthTest login issues a usable bearer token; bad credentials 422; logout revokes tokens
tests/Feature/EventApiTest 401 without a token; full CRUD happy path; update/delete of another user's event is 403
tests/Feature/AttendeeApiTest attendee routes require auth; register/list/unregister flow; PUT/PATCH are 405 (no update verb)
tests/Feature/AttendeeAuthorizationTest only the attendee may cancel their own registration (403 for anyone else, organiser included); {attendee} must belong to {event} or the route is 404
tests/Feature/AttendeeResourceShapeTest the attendee payload is exactly the field list openapi.yaml documents, and included relations go through their own resources
tests/Feature/DuplicateAttendeeTest registering twice is idempotent — the second POST returns the existing row with 200, never a duplicate
tests/Feature/CanLoadRelationshipsTest ?include= parsing: multiple relations, whitespace, empty values, and unknown names that are ignored instead of 500ing
tests/Feature/EventValidationTest StoreEventRequest rules on create and update, and that a client-supplied user_id can never spoof ownership
tests/Feature/EventThrottleTest throttle:60,1 on writes — the 61st write in a minute is 429, reads are unaffected, the window resets
tests/Feature/SendEventRemindersTest the reminder command's 24 h window — past events, both inclusive boundaries, one second past the edge, and events with no attendees
tests/Feature/ScheduleTest app:send-event-reminders is registered on the schedule with a daily (0 0 * * *) expression
tests/Feature/OpenApiSpecTest every /api route + method appears in openapi.yaml — add a route without documenting it and the suite fails
tests/Feature/SmokeTest GET / 200; GET /api/events unauthenticated 401
tests/Unit/EventPolicyTest EventPolicy in isolation — update/delete/restore/forceDelete are owner-only, reads and create are open to any authenticated user

Troubleshooting

Every request 500s with a ParseError in AppServiceProvider

Historical: the repo shipped with a Gate::(policyEvent::class, ...) syntax error that broke every request and artisan command. It is fixed (Gate::policy(Event::class, EventPolicy::class)), but if you ever hit this class of failure, php -l <file> finds the line instantly.

just fresh looks stuck

It isn't — seeding bcrypt-hashes 1000 user passwords and inserts thousands of attendee rows. Expect a couple of minutes.

Connection refused right after just start

The server starts in a background window and can need a few seconds beyond the recipe's 2-second grace. Poll curl.exe -s -o NUL -w "%{http_code}" http://127.0.0.1:8109/ — or use just serve to watch startup in the foreground.

just lint fails on files you never touched

Pre-existing style debt. Keep your own diff clean; clear the backlog with just lint-fix only as a dedicated chore: commit.

More in .docs/06-troubleshooting/common-issues.md.

Project layout

event-management/
├─ app/
│  ├─ Console/Commands/       # SendEventReminders (app:send-event-reminders)
│  ├─ Http/Controllers/Api/   # AuthController, EventController, AttendeeController
│  ├─ Http/Requests/          # LoginRequest, StoreEventRequest
│  ├─ Http/Resources/         # Event/Attendee/User JSON resources
│  ├─ Http/Traits/            # CanLoadRelationships (?include= loading)
│  ├─ Models/                 # User, Event, Attendee
│  ├─ Notifications/          # EventReminderNotification (mail, queued)
│  ├─ Policies/               # EventPolicy (owner-only writes), AttendeePolicy (attendee-only cancel)
│  └─ Providers/              # AppServiceProvider (Gate::policy)
├─ routes/api.php             # login/logout, events, events.attendees, /user
├─ database/                  # migrations, factories, seeders (1000 users / 200 events)
├─ tests/                     # PHPUnit suite (auth, CRUD + ownership, reminders, OpenAPI, smoke)
├─ openapi.yaml               # OpenAPI 3.0 spec — kept honest by OpenApiSpecTest
├─ resources/views/           # welcome.blade.php (only web page — the API landing page)
├─ justfile · setup.ps1       # dev recipes + one-time machine setup
├─ .docs/                     # developer documentation (start at tldr.md)
└─ .claude/                   # project skills, hooks, settings

About

Laravel 12 JSON API with Sanctum auth, event/attendee CRUD, scheduled reminders, and an OpenAPI spec - PHPUnit-tested

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages