This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Hi.Events is an open-source event management and ticketing platform with a Laravel backend and React frontend, using Domain-Driven Design (DDD).
Commands must be executed in the backend docker container:
cd docker/development
docker compose -f docker-compose.dev.yml exec backend php artisan migrate
docker compose -f docker-compose.dev.yml exec backend php artisan generate-domain-objects
docker compose -f docker-compose.dev.yml exec backend php artisan test
docker compose -f docker-compose.dev.yml exec backend php artisan test --filter=TestName
docker compose -f docker-compose.dev.yml exec backend php artisan test --testsuite=Unit
docker compose -f docker-compose.dev.yml exec backend ./vendor/bin/pint --testcd frontend
yarn install
yarn dev:ssr # Development server
yarn build # SSR build
yarn messages:extract # Extract translatable strings
yarn messages:compile # Compile translations
npx tsc --noEmit # TypeScript validationcd docker/development
./start-dev.sh # Unsigned SSL certs
./start-dev.sh --certs=signed # Signed certs with mkcertUnit tests miss wiring bugs — exercise changed endpoints against the dev stack. Base URL https://localhost:8443/api (self-signed — curl -sk). Endpoints are defined in backend/routes/api.php.
# Verified SUPERADMIN account + organizer + LIVE single/recurring events + products
# (paid has waitlist) + promo + affiliate; prints ids and a Bearer token:
docker compose -f docker-compose.dev.yml exec backend php artisan dev:bootstrap
# Manual token (`token` field → `Authorization: Bearer <token>`):
curl -sk -X POST https://localhost:8443/api/auth/login -H "Content-Type: application/json" -d '{"email":"<email>","password":"<password>"}'Gotchas:
- Product upsert requires
product_type(TICKET/GENERAL),type(FREE/PAID/DONATION/TIERED) andproduct_category_id - Promo create requires
applicable_product_ids: [](empty = all products) - Public order complete requires
email_confirmationon the order and on each product entry - Publishing needs
accounts.account_verified_at;/admin/*needs roleSUPERADMIN(dev:bootstraphandles both) - Emails land in Mailpit at
http://localhost:8025(/api/v1/search?query=to:<addr>)
- DON'T add explanatory comments. The code must speak for itself.
- This includes "why" comments justifying a design choice ("X is intentionally omitted because…", "matches the rest of the section rhythm…", "Views live on the per-event-per-day table…", "Online events satisfy the where requirement…"). If you'd write that, rename a variable / extract a function / restructure the code instead, or just leave it implicit.
- Functional annotations are fine: PHPDoc
@throws/@return/@param,// TODO(handle:owner)linked to a tracked task, schema comments inside SQL migrations that future migrations depend on. - Never restate what the next line does. If a reviewer can read the diff and understand it, the comment is noise.
- If you're tempted to leave a comment "for the next agent", don't — write it as a CLAUDE.md note instead.
- Request flow: Action → Handler → Domain Service → Repository
- Handlers can use repositories directly when a service would be overkill
- No Eloquent in handlers or services — Eloquent belongs in repositories only
- Favour composition over inheritance
- Keep code clean, but don't be dogmatic about it
- ALWAYS wrap all translatable strings in
__()helper - Domain Objects are auto-generated via
php artisan generate-domain-objects- never edit manually - Always create unit tests for new features in
backend/tests/Unit/ - DON'T add comments — see the comments rule above. No exceptions for "this seems useful context".
- NEVER leave dead code. Code that has no production callers — unused methods, unused DTO fields, unused constants, columns that are written but never read, classes only called from tests — must be deleted, not left "for future use". This applies to both backend and frontend. If you add a method speculatively, wire it to a real caller in the same change or remove it. The same rule applies after refactors: if something becomes unreferenced, it goes. Confirm with grep before claiming a method or class is reachable.
- ALWAYS sanitize user-provided content with
HtmlPurifierServicebefore storing, especially content rendered as HTML
- Use Spatie Laravel Data package for all new DTOs
- ALWAYS extend
BaseDataObject, notBaseDTO - ALWAYS favor DTOs over arrays when returning multiple values from services
- Always extend
BaseAction.php - ALWAYS use BaseAction response methods:
resourceResponse(),jsonResponse(),errorResponse(),deletedResponse(), etc. Never useresponse()->json()ornew JsonResponse()directly - Always use
isActionAuthorizedfor non-public endpoints - DON'T create actions handling multiple entity types with optional parameters - create separate, focused actions instead
- DO use base classes to share common validation and logic
- DON'T use generic exceptions like
InvalidArgumentExceptionandRuntimeException - DO use custom exceptions (e.g.,
EmailTemplateValidationException,ResourceConflictException) - DO catch custom exceptions in actions and convert to
ValidationException::withMessages()or appropriate error responses
- Favour existing repository methods over creating bespoke ones. E.g., use
findFirstWhere(['event_id' => $eventId])instead of creatingfindByEventId - Bespoke repository methods must wrap their query in
runQuery()— it is the single point that resets$this->model/$this->eagerLoadsafter each call increment()/decrement()arefindOrFail-based and throw for soft-deleted rows; use the where-basedincrementEach()/decrementEach()when the target row may have been deleted (e.g. a promo code deleted after orders used it)
BaseMailis queued andafterCommit()— a mail sent inside a DB transaction that rolls back is silently discarded. Chain->beforeCommit()on the mailable when the send must survive a deliberate rollback (e.g. refund-and-reject webhook paths)- Promo usage,
products.sales_volumeand affiliate sales counters increment only when an order completes — any decrement must be gated onisOrderCompleted()(or equivalent) to stay symmetric
- DO use auto-incrementing integer IDs (
$table->id()), not UUIDs - Use anonymous class syntax for migrations
- Status enums go in
backend/app/DomainObjects/Status/ - Other enums go in
backend/app/DomainObjects/Enums/
- DON'T use
RefreshDatabase- useDatabaseTransactionsinstead - Unit tests extend Laravel's TestCase, not PHPUnit's TestCase
- Use Mockery for mocking
- Unit suite (
tests/Unit/) is for pure isolation tests — no DB, no HTTP, no real container resolution. If a test usesDatabaseTransactions, hits the DB (rawDB::calls, factories that persist, repository methods that query), or boots significant framework state, it's an integration test and belongs intests/Feature/(mirror the path, e.g.tests/Feature/Repository/Eloquent/). Running--testsuite=Unitmust stay fast and DB-free. - Tests run against a dedicated
hievents_testdatabase, configured viabackend/.env.testingand enforced byphpunit.xml. The local docker-compose creates this database automatically viadocker/development/pgsql-init/. If your existing pgsql volume predates this script, create the DB once with:docker compose -f docker-compose.dev.yml exec pgsql psql -U username -d backend -c 'CREATE DATABASE hievents_test OWNER username;' - Database name must end in
_test. Enforced globally by afinalguard intests/TestCase.php::guardAgainstNonTestDatabase()which runs on every test that boots Laravel — no per-test opt-in needed and no way to bypass.
- This is a SSR app - ensure safe usage of
windowanddocumentobjects - Favour using existing components over creating new ones
- DON'T include unnecessary React imports
- ALWAYS add translations when adding new user-facing strings - use Lingui
tfunction orTranscomponent - IMMEDIATELY after adding translatable strings, add translations for all supported languages (use
/translationsskill for the workflow)
- Use React Query for all API interactions
- Query example:
frontend/src/queries/useGetCapacityAssignment.ts - Mutation example:
frontend/src/mutations/useCreateAffiliate.ts
- Use Mantine UI components for UI elements
- Prefer SCSS modules over Mantine layout components for layout styling
- There is a Playwright E2E suite in
e2e/(seee2e/README.md). It runs the real stack (Laravel + SSR frontend + Postgres + Redis + Mailpit) in Docker. - To test uncommitted changes, run specs against the dev stack — the hermetic e2e stack bakes source into images and
docker compose upnever rebuilds them. Frome2e/:E2E_BASE_URL=https://localhost:8443 MAILPIT_URL=http://localhost:8025 E2E_SAAS_MODE=true npx playwright test <spec>
E2E_SAAS_MODE=trueis required (the dev stack requires email verification; the fixture only confirms via Mailpit in SaaS mode), a queue worker must be running to deliver the verification emails, and superadmin-dependent specs need a one-timephp artisan dev:bootstrap --email=superadmin@e2e.test --password='SuperAdminPass123!'. See "Against the running dev stack" ine2e/README.md. - When you add or meaningfully change a user-facing flow, add or update an E2E spec for it where practical. Follow the existing pattern: arrange data via the API/
factory, drive only the flow under test through the UI with a thin page object, and assert on real page content (the created/edited item appears), not just a URL change. Tag fast, load-bearing checks with@smoke. - Not everything needs E2E — reserve it for real user journeys (create/edit/complete flows). Pure logic belongs in backend unit/feature tests instead.
- Add a
data-testidto interactive elements the E2E suite needs to drive — primarily buttons (open-modal triggers, submit/save), menu items, and custom widgets with no accessible label (e.g.CustomSelect, which takes adataTestIdprop that lands on its target and options). This is not required for every element: text inputs with a unique<label>are found by role/label instead, so don't add IDs there. - Convention: kebab-case
<feature>-<element>, e.g.promo-code-create-button,webhook-submit-button,product-edit-menu-item. ForCustomSelect, options are auto-derived as<dataTestId>-option-<value>. - Only add IDs for elements a test actually interacts with; don't blanket-annotate new UI.
- DON'T use
showNotificationfrom@mantine/notifications - DO use
showSuccess,showErrorfromfrontend/src/utilites/notifications.tsx - DO use
useFormErrorResponseHandlerfromfrontend/src/hooks/useFormErrorResponseHandler.tsxfor validation errors - DO handle errors in parent components, not in reusable components
- NEVER add
Co-Authored-By: Claudeor any AI attribution lines to commit messages.
- Create migration:
php artisan make:migration create_XXX_table - Run migration:
php artisan migrate - Regenerate Domain Objects:
php artisan generate-domain-objects
- Frontend:
cd frontend && npx tsc --noEmit - Backend:
docker compose -f docker-compose.dev.yml exec backend php artisan test --testsuite=Unit