# PreRegister — Development Prompt & Architecture Specification > **Purpose of this document:** This is the master prompt and specification for building "PreRegister" — a Laravel application for festival ticket pre-registration. Feed this document to **Claude Code** or **Cursor** to scaffold, develop, and iterate on the application. Every section is written as an actionable instruction set. --- ## 0. Context & Role You are a **senior full-stack software architect and Laravel expert**. You write clean, production-ready PHP 8.2+ / Laravel 11 code. You follow PSR-12, use strict typing, and prefer explicit over clever. You build with security, scalability, and maintainability in mind — even for "simple" apps. When developing this application: - **Think before you code.** Read the full spec first. Plan the migration order, model relationships, and route structure before writing a single file. - **Work incrementally.** Commit logically grouped changes. Don't dump the entire app in one pass. - **Test as you go.** After each feature, verify it works. Run `php artisan migrate:fresh --seed` regularly. - **Ask clarifying questions** if a requirement is ambiguous — don't guess. --- ## 1. Project Overview **PreRegister** is a multi-tenant pre-registration system for festival ticket sales. Event organizers create branded landing pages where visitors can sign up for early/discounted ticket access. Subscriber data is stored locally and optionally synced to Mailwizz (email marketing platform). ### Core User Flows ``` ┌─────────────────────────────────────────────────────────┐ │ BACKEND (authenticated) │ │ │ │ Superadmin → manages Users │ │ User → creates/edits Pre-Registration Pages │ │ → configures Mailwizz integration │ │ → views/exports subscribers │ └─────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ FRONTEND (public, unique URL per page) │ │ │ │ Guest → sees countdown / form / expired message │ │ → fills in form (name, email, phone) │ │ → gets thank-you confirmation │ │ → data stored in DB + optionally pushed to │ │ Mailwizz with tag/source tracking │ └─────────────────────────────────────────────────────────┘ ``` --- ## 2. Tech Stack & Environment | Layer | Technology | |---|---| | Language | PHP 8.2+ (strict types everywhere) | | Framework | Laravel 11.x | | Database | MySQL 8.x | | Frontend (backend UI) | Blade + Tailwind CSS 3.x + Alpine.js 3.x | | Frontend (public pages) | Blade + Tailwind CSS + Alpine.js (countdown, form validation) | | Auth | Laravel Breeze (Blade stack) | | File storage | Local disk (`storage/app/public`, symlinked) | | HTTP client | Laravel HTTP facade (for Mailwizz API) | | Queue | `database` driver (for async Mailwizz sync) | | Cache | `file` driver (default) | ### Development Environment ```bash # Prerequisites php >= 8.2 with extensions: mbstring, xml, curl, mysql, gd/imagick composer >= 2.7 node >= 20 (for Tailwind/Vite) mysql >= 8.0 # Bootstrap composer create-project laravel/laravel preregister cd preregister composer require laravel/breeze --dev php artisan breeze:install blade npm install ``` --- ## 3. Database Schema Design migrations in this exact order. Use `$table->id()` (unsigned big int) for all primary keys. Use UUIDs for public-facing identifiers (slugs/URLs). ### Migration 1: Users table modification Laravel ships with a `users` table. Add a `role` enum column. ```php // database/migrations/xxxx_add_role_to_users_table.php Schema::table('users', function (Blueprint $table) { $table->enum('role', ['superadmin', 'user'])->default('user')->after('email'); }); ``` > **Note:** "Guest" is not a database role — it's an unauthenticated visitor on the public frontend. ### Migration 2: Pre-Registration Pages ```php Schema::create('preregistration_pages', function (Blueprint $table) { $table->id(); $table->uuid('slug')->unique(); // used in public URL: /r/{slug} $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->string('title'); // internal title (backend) $table->string('heading'); // public heading on the page $table->text('intro_text')->nullable(); // shown before start_date (with countdown) $table->text('thank_you_message')->nullable(); // shown after successful registration $table->text('expired_message')->nullable(); // shown after end_date $table->string('ticketshop_url')->nullable(); // link shown after expiration $table->dateTime('start_date'); $table->dateTime('end_date'); $table->boolean('phone_enabled')->default(false); // show phone field yes/no $table->string('background_image')->nullable(); // path in storage $table->string('logo_image')->nullable(); // path in storage $table->boolean('is_active')->default(true); $table->timestamps(); }); ``` ### Migration 3: Subscribers ```php Schema::create('subscribers', function (Blueprint $table) { $table->id(); $table->foreignId('preregistration_page_id')->constrained()->cascadeOnDelete(); $table->string('first_name'); $table->string('last_name'); $table->string('email'); $table->string('phone')->nullable(); $table->boolean('synced_to_mailwizz')->default(false); $table->timestamp('synced_at')->nullable(); $table->timestamps(); // Prevent duplicate emails per page $table->unique(['preregistration_page_id', 'email']); }); ``` ### Migration 4: Mailwizz Configuration ```php Schema::create('mailwizz_configs', function (Blueprint $table) { $table->id(); $table->foreignId('preregistration_page_id')->constrained()->cascadeOnDelete(); $table->text('api_key'); // encrypted at rest $table->string('list_uid'); // Mailwizz list UID $table->string('list_name')->nullable(); // display name for reference $table->string('field_email')->default('EMAIL'); // Mailwizz tag for email $table->string('field_first_name')->default('FNAME'); $table->string('field_last_name')->default('LNAME'); $table->string('field_phone')->nullable(); // e.g. 'PHONE' $table->string('tag_field')->nullable(); // e.g. 'TAGS' — the checkboxlist field tag $table->string('tag_value')->nullable(); // e.g. 'ezf2026-preregister' $table->timestamps(); }); ``` > **Encryption:** The `api_key` column MUST use Laravel's `encrypted` cast on the model. Never store API keys in plain text. --- ## 4. Models & Relationships ### `User` model ``` - hasMany PreregistrationPage - Has `role` attribute (superadmin | user) - Add `isSuperadmin()` and `isUser()` helper methods ``` ### `PreregistrationPage` model ``` - belongsTo User - hasMany Subscriber - hasOne MailwizzConfig - Uses UUID slug for public URL resolution (route model binding on 'slug') - Add accessors: isBeforeStart(), isActive(), isExpired() - Add scope: scopeActive() - Cast start_date and end_date to datetime ``` ### `Subscriber` model ``` - belongsTo PreregistrationPage ``` ### `MailwizzConfig` model ``` - belongsTo PreregistrationPage - Cast 'api_key' => 'encrypted' ``` --- ## 5. Authentication & Authorization ### Roles | Role | Access | |---|---| | `superadmin` | Full backend access. Can manage users (CRUD). Can see/edit all pages. | | `user` | Backend access. Can only manage own pages and see own subscribers. | | `guest` | Public frontend only. No authentication required. | ### Implementation 1. Install Laravel Breeze (Blade stack) for login/register scaffolding. 2. **Disable public registration** — only superadmin can create users. Remove the registration routes or gate them behind superadmin middleware. 3. Create a `CheckRole` middleware: ```php // app/Http/Middleware/CheckRole.php public function handle($request, Closure $next, string ...$roles): Response { if (! in_array($request->user()->role, $roles)) { abort(403); } return $next($request); } ``` 4. Register in `bootstrap/app.php` as alias `role`. 5. Apply to routes: ```php Route::middleware(['auth', 'role:superadmin'])->group(function () { // User management routes }); Route::middleware(['auth', 'role:superadmin,user'])->group(function () { // Pre-registration page management routes }); ``` 6. Add a **Policy** for `PreregistrationPage`: - `superadmin` can do everything. - `user` can only view/edit/delete own pages. --- ## 6. Route Structure ```php // ─── Public (no auth) ──────────────────────────────── Route::get('/r/{slug}', [PublicPageController::class, 'show'])->name('public.page'); Route::post('/r/{slug}/subscribe', [PublicPageController::class, 'subscribe'])->name('public.subscribe'); // ─── Backend (auth required) ───────────────────────── Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(function () { Route::get('/dashboard', DashboardController::class)->name('dashboard'); // Pre-registration pages (CRUD) Route::resource('pages', PageController::class); // Subscribers (nested under pages) Route::get('pages/{page}/subscribers', [SubscriberController::class, 'index'])->name('pages.subscribers.index'); Route::get('pages/{page}/subscribers/export', [SubscriberController::class, 'export'])->name('pages.subscribers.export'); // Mailwizz configuration (nested under pages) Route::get('pages/{page}/mailwizz', [MailwizzController::class, 'edit'])->name('pages.mailwizz.edit'); Route::put('pages/{page}/mailwizz', [MailwizzController::class, 'update'])->name('pages.mailwizz.update'); Route::delete('pages/{page}/mailwizz', [MailwizzController::class, 'destroy'])->name('pages.mailwizz.destroy'); // Mailwizz AJAX endpoints (for dynamic field loading) Route::post('mailwizz/lists', [MailwizzApiController::class, 'lists'])->name('mailwizz.lists'); Route::post('mailwizz/fields', [MailwizzApiController::class, 'fields'])->name('mailwizz.fields'); // User management (superadmin only) Route::middleware('role:superadmin')->group(function () { Route::resource('users', UserController::class)->except(['show']); }); }); ``` --- ## 7. Backend Features — Detailed Specifications ### 7.1 Dashboard Simple overview showing: - Total pages (own pages for `user`, all for `superadmin`) - Total subscribers across pages - Active pages (currently within start/end date window) - Quick links to create new page ### 7.2 Pre-Registration Page CRUD **Create/Edit form fields:** | Field | Type | Validation | |---|---|---| | Title | text | required, max:255 | | Heading | text | required, max:255 | | Intro Text | textarea (rich or plain) | nullable | | Thank You Message | textarea | nullable | | Expired Message | textarea | nullable | | Ticket Shop URL | url | nullable, valid URL | | Start Date | datetime-local | required, valid date | | End Date | datetime-local | required, after:start_date | | Phone Enabled | checkbox | boolean | | Background Image | file upload | nullable, image, max:5MB, jpg/png/webp | | Logo Image | file upload | nullable, image, max:2MB, jpg/png/webp/svg | | Active | checkbox | boolean | **On create:** - Auto-generate a UUID slug (use `Str::uuid()`) - Store uploaded images via `Storage::disk('public')->put(...)` - Display the full public URL to the user: `{APP_URL}/r/{slug}` **Index view:** - Table with: Title, Status (before start / active / expired), Start Date, End Date, Subscriber Count, Actions (edit, view subscribers, public URL copy button, delete) - Superadmin sees all pages; User sees only own pages ### 7.3 Subscribers View - Table: First Name, Last Name, Email, Phone (if enabled), Registered At, Synced to Mailwizz (yes/no icon) - Pagination (25 per page) - CSV export button - Search/filter by name or email ### 7.4 Mailwizz Configuration (per page) This is a **multi-step configuration wizard** inside the page edit area (can be a separate tab or section). Use Alpine.js for dynamic behavior. **Step-by-step flow:** 1. **Enter API Key** - Input field for the Mailwizz API key - Display instruction text: *"First, create a mailing list in Mailwizz with the required custom fields. Make sure to add a custom field of the type 'Checkbox List' with an available option value that will be used to track the source of this pre-registration."* - "Connect" button → validates the API key by fetching lists 2. **Select List** - On valid API key: make a server-side call to `GET https://www.mailwizz.nl/api/lists` - Header: `X-Api-Key: {api_key}` - Display available lists in a dropdown (show `general.name`, store `general.list_uid`) - "Load Fields" button → fetches fields for the selected list 3. **Map Fields** - Fetch fields from `GET https://www.mailwizz.nl/api/lists/{list_uid}/fields` - Header: `X-Api-Key: {api_key}` - Display mapping dropdowns: | Local Field | Mailwizz Field Filter | Map To | |---|---|---| | Email | Show fields where `type.identifier` is `text` AND (`tag` contains `EMAIL` or is standard email field) | Dropdown of matching fields (display `label`, store `tag`) | | First Name | Show fields where `type.identifier` is `text` | Dropdown | | Last Name | Show fields where `type.identifier` is `text` | Dropdown | | Phone | Show fields where `type.identifier` is `phonenumber` | Dropdown (only shown if phone_enabled) | | Tag / Source | Show fields where `type.identifier` is `checkboxlist` | Dropdown of checkboxlist fields | 4. **Select Tag Value** - After selecting the checkboxlist field, load its `options` object - Display available options as radio buttons or dropdown (show the value label, store the key) - Example: key = `ezf2026-preregister`, label = `Echt Zomer Feesten 2026 - Pré-register` 5. **Save Configuration** - Store all mappings in the `mailwizz_configs` table - Show success message with summary of the configuration **AJAX Controller (`MailwizzApiController`):** ```php // POST /admin/mailwizz/lists // Receives: api_key // Returns: list of Mailwizz lists public function lists(Request $request): JsonResponse { $request->validate(['api_key' => 'required|string']); $response = Http::withHeaders([ 'X-Api-Key' => $request->api_key, ])->get('https://www.mailwizz.nl/api/lists'); if ($response->failed()) { return response()->json(['error' => 'Invalid API key or connection failed'], 422); } return response()->json($response->json()); } // POST /admin/mailwizz/fields // Receives: api_key, list_uid // Returns: list fields for the given list ``` --- ## 8. Frontend (Public Pages) — Detailed Specifications ### 8.1 Page States The public page at `/r/{slug}` has three states based on current date/time: #### State 1: Before Start Date (Countdown) ``` ┌──────────────────────────────────────────┐ │ [Full-screen background image] │ │ │ │ [Logo] │ │ │ │ {heading} │ │ │ │ {intro_text} │ │ │ │ ┌──────────────────────────┐ │ │ │ DD : HH : MM : SS │ │ │ │ days hrs mins secs │ │ │ └──────────────────────────┘ │ │ │ └──────────────────────────────────────────┘ ``` - Use Alpine.js for a live countdown timer - When countdown reaches zero, auto-refresh to show the form (or use Alpine to reactively switch state) #### State 2: Active (Registration Form) ``` ┌──────────────────────────────────────────┐ │ [Full-screen background image] │ │ │ │ [Logo] │ │ │ │ {heading} │ │ │ │ ┌──────────────────────────┐ │ │ │ First Name [________] │ │ │ │ Last Name [________] │ │ │ │ Email [________] │ │ │ │ Phone* [________] │ *if on │ │ │ │ │ │ │ [ Register ] │ │ │ └──────────────────────────┘ │ │ │ └──────────────────────────────────────────┘ ``` - Client-side validation with Alpine.js - Server-side validation in controller - On success: show `{thank_you_message}` (replace the form, no page reload — use Alpine) - On duplicate email error: show a friendly message ("You are already registered for this event") #### State 3: Expired ``` ┌──────────────────────────────────────────┐ │ [Full-screen background image] │ │ │ │ [Logo] │ │ │ │ {heading} │ │ │ │ {expired_message} │ │ │ │ [Visit Ticket Shop →] │ (if ticketshop_url set) │ │ └──────────────────────────────────────────┘ ``` ### 8.2 Styling Requirements - **Background image:** Full-screen cover, centered, no-repeat. Use CSS `background-size: cover; background-position: center;` on the `` or a wrapper div. - **Overlay:** Add a semi-transparent dark overlay (e.g., `bg-black/50`) so text is readable on any image. - **Content card:** Center a card/container (max-width ~500px) with slight backdrop blur and padding. - **Logo:** Displayed above the heading, max-height ~80px, centered. - **Typography:** White text, clean sans-serif. Heading large (text-3xl), body text readable. - **Mobile responsive:** The form and all states must work perfectly on mobile. ### 8.3 Form Submission Logic ```php // PublicPageController@subscribe public function subscribe(Request $request, string $slug) { $page = PreregistrationPage::where('slug', $slug) ->where('is_active', true) ->firstOrFail(); // Verify page is in active window abort_if(now()->lt($page->start_date) || now()->gt($page->end_date), 403); $validated = $request->validate([ 'first_name' => 'required|string|max:255', 'last_name' => 'required|string|max:255', 'email' => 'required|email|max:255', 'phone' => $page->phone_enabled ? 'required|string|max:20' : 'nullable', ]); // Check duplicate $exists = $page->subscribers()->where('email', $validated['email'])->exists(); if ($exists) { return response()->json([ 'success' => false, 'message' => 'This email address is already registered.', ], 422); } // Store subscriber $subscriber = $page->subscribers()->create($validated); // Dispatch Mailwizz sync job (if configured) if ($page->mailwizzConfig) { SyncSubscriberToMailwizz::dispatch($subscriber); } return response()->json([ 'success' => true, 'message' => $page->thank_you_message ?? 'Thank you for registering!', ]); } ``` --- ## 9. Mailwizz Integration — Subscriber Sync Logic > **CRITICAL:** This is the most complex part of the app. Implement as a queued job. ### Job: `SyncSubscriberToMailwizz` ``` App\Jobs\SyncSubscriberToMailwizz - Receives: Subscriber $subscriber - Queue: 'mailwizz' - Retries: 3, backoff: [10, 30, 60] ``` ### Sync Algorithm ``` 1. Load the subscriber + related page + mailwizz config 2. Decrypt the API key from mailwizz_configs 3. SEARCH for existing subscriber in Mailwizz: GET https://www.mailwizz.nl/api/lists/{list_uid}/subscribers/search-by-email Query: EMAIL={subscriber.email} Header: X-Api-Key: {api_key} 4. IF subscriber does NOT exist (status: "error"): → CREATE subscriber POST https://www.mailwizz.nl/api/lists/{list_uid}/subscribers Body (form-data): {email_field_tag} = subscriber.email {fname_field_tag} = subscriber.first_name {lname_field_tag} = subscriber.last_name {phone_field_tag} = subscriber.phone (if mapped) {tag_field_tag} = [configured_tag_value] (send as array) 5. IF subscriber DOES exist (status: "success"): → FETCH full subscriber data GET https://www.mailwizz.nl/api/lists/{list_uid}/subscribers/{subscriber_uid} → Parse existing tag value from the tag field (comma-separated string) → Append configured_tag_value if not already present → Example: existing = "tag1,tag2" → append → "tag1,tag2,ezf2026-preregister" → UPDATE subscriber PUT https://www.mailwizz.nl/api/lists/{list_uid}/subscribers/{subscriber_uid} Body (form-data): {email_field_tag} = subscriber.email {fname_field_tag} = subscriber.first_name {lname_field_tag} = subscriber.last_name {phone_field_tag} = subscriber.phone (if mapped) {tag_field_tag} = [merged_tag_values_as_array] 6. On success: update subscriber record $subscriber->update(['synced_to_mailwizz' => true, 'synced_at' => now()]); 7. On failure: log the error, let the job retry ``` ### Important Mailwizz API Notes - **Base URL:** `https://www.mailwizz.nl/api` - **Auth:** Header `X-Api-Key: {key}` - **Data format:** Send as `form-data` (use `->asForm()` with Laravel HTTP client) - **Checkboxlist values** are stored as comma-separated strings in Mailwizz. When updating, you must: 1. Load existing value 2. Split by comma 3. Add the new value if not present 4. Send back as an **array** in the form data (Mailwizz expects array input for checkboxlist, it stores as CSV internally) - **Typo in docs:** The update endpoint in the Mailwizz docs has a typo (`/llists/` instead of `/lists/`). Use the correct URL: `https://www.mailwizz.nl/api/lists/{list_uid}/subscribers/{subscriber_uid}` ### Service Class Create a dedicated `App\Services\MailwizzService` class to encapsulate all API communication: ```php class MailwizzService { public function __construct( private string $apiKey, private string $baseUrl = 'https://www.mailwizz.nl/api' ) {} public function getLists(): array public function getListFields(string $listUid): array public function searchSubscriber(string $listUid, string $email): ?array public function getSubscriber(string $listUid, string $subscriberUid): ?array public function createSubscriber(string $listUid, array $data): array public function updateSubscriber(string $listUid, string $subscriberUid, array $data): array } ``` Each method should: - Use `Http::withHeaders(['X-Api-Key' => $this->apiKey])->asForm()` - Throw descriptive exceptions on failure - Log all API interactions at `debug` level --- ## 10. Security Checklist - [ ] **CSRF protection** on all forms (Blade `@csrf` directive) - [ ] **API key encryption** using Laravel's `encrypted` cast — never store in plain text - [ ] **File upload validation** — restrict to image types, enforce max size - [ ] **Authorization policies** — users can only access own resources - [ ] **Rate limiting** on public subscription endpoint (prevent spam): `throttle:10,1` (10 per minute) - [ ] **Input sanitization** — XSS protection on all user-provided content displayed in Blade - [ ] **Slug unpredictability** — UUID slugs prevent enumeration - [ ] **SQL injection** — use Eloquent/query builder only, never raw queries with user input - [ ] **Mass assignment protection** — define `$fillable` on every model - [ ] **HTTPS only** — set `APP_URL` with `https://`, enable `secure` cookies in production --- ## 11. File & Folder Structure ``` app/ ├── Http/ │ ├── Controllers/ │ │ ├── Admin/ │ │ │ ├── DashboardController.php │ │ │ ├── PageController.php │ │ │ ├── SubscriberController.php │ │ │ ├── MailwizzController.php │ │ │ ├── MailwizzApiController.php │ │ │ └── UserController.php │ │ └── PublicPageController.php │ ├── Middleware/ │ │ └── CheckRole.php │ └── Requests/ │ ├── StorePageRequest.php │ ├── UpdatePageRequest.php │ └── SubscribeRequest.php ├── Jobs/ │ └── SyncSubscriberToMailwizz.php ├── Models/ │ ├── User.php │ ├── PreregistrationPage.php │ ├── Subscriber.php │ └── MailwizzConfig.php ├── Policies/ │ └── PreregistrationPagePolicy.php ├── Services/ │ └── MailwizzService.php └── Providers/ resources/views/ ├── admin/ │ ├── layouts/ │ │ └── app.blade.php (backend layout with sidebar nav) │ ├── dashboard.blade.php │ ├── pages/ │ │ ├── index.blade.php │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ └── _form.blade.php (shared form partial) │ ├── subscribers/ │ │ └── index.blade.php │ ├── mailwizz/ │ │ └── edit.blade.php (configuration wizard) │ └── users/ │ ├── index.blade.php │ ├── create.blade.php │ └── edit.blade.php └── public/ └── page.blade.php (the public pre-registration page) database/ ├── migrations/ │ ├── xxxx_add_role_to_users_table.php │ ├── xxxx_create_preregistration_pages_table.php │ ├── xxxx_create_subscribers_table.php │ └── xxxx_create_mailwizz_configs_table.php └── seeders/ └── SuperadminSeeder.php (creates initial superadmin account) ``` --- ## 12. Seeder: Initial Superadmin ```php // database/seeders/SuperadminSeeder.php public function run(): void { User::updateOrCreate( ['email' => 'admin@preregister.app'], [ 'name' => 'Super Admin', 'password' => Hash::make('changeme123!'), 'role' => 'superadmin', 'email_verified_at' => now(), ] ); } ``` > **Post-deploy:** Change the password immediately. --- ## 13. Key Implementation Details ### 13.1 Image Upload Handling ```php // In PageController store/update methods: if ($request->hasFile('background_image')) { // Delete old image if replacing if ($page->background_image) { Storage::disk('public')->delete($page->background_image); } $path = $request->file('background_image')->store('pages/backgrounds', 'public'); $page->background_image = $path; } ``` Run `php artisan storage:link` to create the public symlink. ### 13.2 Alpine.js Countdown Timer ```html
00 days
00 hours
00 minutes
00 seconds
``` ### 13.3 Form Submission with Alpine.js ```html

``` ### 13.4 CSV Export ```php // SubscriberController@export public function export(PreregistrationPage $page) { $this->authorize('view', $page); $subscribers = $page->subscribers()->orderBy('created_at')->get(); return response()->streamDownload(function () use ($subscribers, $page) { $handle = fopen('php://output', 'w'); fputcsv($handle, ['First Name', 'Last Name', 'Email', 'Phone', 'Registered At']); foreach ($subscribers as $sub) { fputcsv($handle, [ $sub->first_name, $sub->last_name, $sub->email, $sub->phone, $sub->created_at->toDateTimeString(), ]); } fclose($handle); }, "subscribers-{$page->slug}.csv"); } ``` --- ## 14. Coding Standards & Conventions ### For AI assistants (Claude Code & Cursor) Follow these rules strictly: 1. **PHP strict types** — every PHP file starts with `declare(strict_types=1);` 2. **Type hints everywhere** — method parameters, return types, properties 3. **No magic strings** — use constants or enums for repeated values (roles, statuses) 4. **Form Request classes** — all validation in dedicated Form Request classes, not inline 5. **Single responsibility** — controllers are thin, business logic in services/jobs 6. **Blade components** — use `` syntax for reusable UI elements 7. **No `dd()` in committed code** — use proper logging 8. **Database transactions** — wrap multi-step operations in `DB::transaction()` 9. **Error handling** — never swallow exceptions. Log and re-throw or return user-friendly error. 10. **Comments** — only for *why*, never for *what*. The code should be self-documenting. ### Git Workflow When using Claude Code or Cursor in agentic mode: ``` Commit after each logical unit: 1. Migrations + Models → "feat: add database schema and models" 2. Auth + Middleware + Policies → "feat: add auth, roles and authorization" 3. Backend CRUD for pages → "feat: add pre-registration page management" 4. Public frontend → "feat: add public registration pages" 5. Subscriber management → "feat: add subscriber list and export" 6. Mailwizz configuration UI → "feat: add Mailwizz configuration wizard" 7. Mailwizz sync job → "feat: add Mailwizz subscriber sync" 8. User management (superadmin) → "feat: add user management for superadmin" 9. Polish, validation, error handling → "fix: add validation and error handling" ``` --- ## 15. Development Sequence (Step-by-Step for AI) > **Instructions for Claude Code / Cursor:** Execute these steps in order. Complete each step fully before moving to the next. Test each step. ### Phase 1: Foundation ``` Step 1: Create the Laravel project and install dependencies - laravel/breeze (blade) - tailwindcss, alpinejs (via breeze/vite) - Configure .env for MySQL Step 2: Create all migrations (in the order specified in Section 3) - Run php artisan migrate Step 3: Create all Eloquent models with relationships, casts, fillable (Section 4) Step 4: Set up authentication - Modify Breeze to disable public registration - Add CheckRole middleware - Create PreregistrationPagePolicy - Create SuperadminSeeder and run it Step 5: Set up route structure (Section 6) ``` ### Phase 2: Backend ``` Step 6: Build the admin layout (sidebar, navigation, responsive) Step 7: Build the Dashboard page Step 8: Build Pre-Registration Page CRUD - Index with status indicators - Create/Edit form with image uploads - Delete with confirmation - Storage link for public images Step 9: Build Subscriber index - Paginated table - Search/filter - CSV export Step 10: Build User Management (superadmin only) - CRUD for users with role assignment ``` ### Phase 3: Public Frontend ``` Step 11: Build the public page view - Three states: countdown, form, expired - Full-screen background + overlay + centered card - Alpine.js countdown timer - Alpine.js form submission (AJAX) - Mobile responsive Step 12: Build the subscription endpoint - Validation - Duplicate check - Store subscriber - Return JSON for Alpine.js ``` ### Phase 4: Mailwizz Integration ``` Step 13: Create MailwizzService class Step 14: Build Mailwizz configuration wizard - API key input + validation - List selection (AJAX) - Field mapping (AJAX) - Tag/source selection - Save configuration Step 15: Create SyncSubscriberToMailwizz job - Search existing subscriber - Create or update with tag merging - Error handling and retries - Mark as synced Step 16: Wire up the job dispatch in PublicPageController@subscribe ``` ### Phase 5: Polish ``` Step 17: Add rate limiting to public endpoints Step 18: Add flash messages / toast notifications in backend Step 19: Add form validation error display (Blade error bags) Step 20: Test the full flow end-to-end Step 21: Write a README.md with setup instructions ``` --- ## 16. Environment Variables ```env APP_NAME=PreRegister APP_URL=https://preregister.yourdomain.com DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=preregister DB_USERNAME=preregister DB_PASSWORD= QUEUE_CONNECTION=database # No Mailwizz env vars — API keys are per-page and stored encrypted in DB ``` --- ## 17. Deployment Notes - Run `php artisan storage:link` to expose uploads - Run `php artisan queue:work --queue=mailwizz` (or configure Supervisor) for async Mailwizz sync - Run `php artisan migrate --seed` on first deploy - Set up a cron for `php artisan schedule:run` (for any future scheduled tasks) - Ensure `APP_KEY` is set and backed up - Configure HTTPS via reverse proxy (Nginx/Caddy) --- ## Appendix A: Mailwizz API Quick Reference | Action | Method | URL | Notes | |---|---|---|---| | Get all lists | GET | `/api/lists` | Paginated | | Get list fields | GET | `/api/lists/{list_uid}/fields` | All custom fields | | Search subscriber | GET | `/api/lists/{list_uid}/subscribers/search-by-email?EMAIL={email}` | Returns `subscriber_uid` | | Get subscriber | GET | `/api/lists/{list_uid}/subscribers/{subscriber_uid}` | Full subscriber data | | Create subscriber | POST | `/api/lists/{list_uid}/subscribers` | Form-data body | | Update subscriber | PUT | `/api/lists/{list_uid}/subscribers/{subscriber_uid}` | Form-data body | **All requests require header:** `X-Api-Key: {your_api_key}` **Checkboxlist field handling:** - Stored internally as comma-separated string: `"tag1,tag2,tag3"` - When sending via API, send as array: `TAGS[]=tag1&TAGS[]=tag2` - When reading, split the string by comma to get individual values - When updating, merge existing values with new value, deduplicate, send as array --- ## Appendix B: Cursor-Specific Instructions When using this prompt with **Cursor**: 1. Open the project folder in Cursor 2. Add this entire document as context (paste in chat or reference as file) 3. Use `@Codebase` to let Cursor see existing files 4. Work through the Development Sequence (Section 15) step by step 5. Use Cursor's "Apply" feature to write directly to files 6. After each phase, run `php artisan serve` and test in browser 7. Use the terminal panel to run Artisan commands ### Cursor Rules (add to `.cursorrules`) ``` You are working on PreRegister, a Laravel 11 application. - Always use PHP 8.2+ strict types - Follow PSR-12 coding standards - Use Laravel conventions (route model binding, form requests, policies) - Use Blade + Tailwind + Alpine.js (no React, no Vue, no Livewire) - All controller methods must have return type hints - Use the Service pattern for external API calls (MailwizzService) - Never expose API keys in logs, responses, or frontend code - Test after each feature: php artisan serve + browser check ``` --- ## Appendix C: Claude Code-Specific Instructions When using this prompt with **Claude Code**: 1. Start in the project root directory 2. Read this entire document first before writing any code 3. Execute the Development Sequence (Section 15) in order 4. Use `bash` to run Artisan commands and verify state 5. Create files with proper directory structure 6. After creating migrations, always run `php artisan migrate` 7. After modifying routes, run `php artisan route:list` to verify 8. Commit after each logical step with conventional commit messages ### Claude Code Tips - When creating Blade views, always create the layout first - When creating controllers, create the Form Request classes at the same time - When adding a new route, immediately create the controller method (even as a stub) - For the Mailwizz wizard: build the backend API endpoints first, then the Alpine.js frontend - Use `php artisan tinker` to test model relationships - Use `php artisan make:*` commands to generate boilerplate where possible