# Proposal Management System — API Reference

> Generated from the current Laravel route files, FormRequest validators and frontend service calls on 19 October 2026. Treat this as the integration reference for the present codebase; it is not an OpenAPI-generated contract.

## 1. Service overview

| Layer | Technology | Responsibility |
| --- | --- | --- |
| Frontend | React 19, Vite, Redux Toolkit, React Router | SPA, permission-aware interface, API client |
| Gateway | Nginx | Same-origin routing of `/api`, `/storage`, `/sanctum` to Laravel and all other paths to the SPA |
| Backend | Laravel 13, PHP 8.4, Sanctum, Spatie Permission, Nwidart Modules | REST API, RBAC, files, workflows, notifications |
| Data | MySQL 8 | business records, jobs, proposals, leads, targets, audit logs |
| Integrations | Gemini, Pusher, Upwork/ERP endpoints | extraction/AI, real-time events, external data |

The base URL is `https://<host>/api`; all paths below are relative to it. Module route providers prefix their own routes with `/api`, so their listed paths do not use a second version prefix. The legacy `v1/upworks` resource is the only explicitly versioned module API.

## 2. Authentication and response contract

### Authentication

Public endpoints: `POST /login`, `POST /register`, `POST /auth/forgot-password`, `POST /auth/reset-password`.

All other routes require a Sanctum personal-access token:

```http
Accept: application/json
Authorization: Bearer <token>
Content-Type: application/json
```

Use `multipart/form-data` for file/image endpoints. Core routes additionally use `active.user`, which rejects disabled users and revokes the token. Module routes currently use `auth:sanctum` but do **not** consistently include this active-user middleware; see [Known implementation gaps](#9-known-implementation-gaps).

### Standard envelope

Most endpoints use the following envelope:

```json
{ "success": true, "message": "…", "data": {} }
```

Errors normally use:

```json
{ "success": false, "message": "…", "errors": {} }
```

Typical statuses are `200`, `201`, `400`, `401`, `403`, `404`, `422` and `500`. A few endpoints may return a direct file stream, paginated `data`, or a validator-specific `errors` array; clients must not assume every response has the same data shape.

### Permission notation

`auth` means valid Sanctum token. A named permission such as `proposals.create` is enforced by Spatie middleware. `A | B` means either named permission is allowed. `admin | X` means the admin role or the permission. Role permissions should be read from `GET /permissions` and `GET /roles`, not hard-coded in a third-party client.

## 3. Core account, administration and notification API

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| POST | `/login` | Public | Authenticate; returns user and bearer token. |
| POST | `/register` | Public | Register a user; returns user and bearer token. Deployment should normally restrict this externally. |
| POST | `/auth/forgot-password` | Public | Send/reset-password workflow request. |
| POST | `/auth/reset-password` | Public | Reset password using supplied reset token. |
| GET | `/user` | auth + active | Current user with roles/permissions. |
| POST | `/logout` | auth + active | Revoke current token. |
| PUT | `/profile/update` | `profile.update` | Update current name/email. |
| PUT | `/profile/password` | `profile.change_password` | Change current password. |
| GET | `/app/config` | auth + active | Client-visible app configuration. |
| GET | `/active/users` | auth + active | Role/category filtered user lookup (`role`, `category`). |
| GET | `/users/{user}/submission-stats` | auth + active | User submission statistics. |
| GET/PUT | `/shift-config` | `shifts.view` / `shifts.edit` | Read or save global shift configuration. |
| GET/POST | `/gemini-config` | `gemini_config.view` / `gemini_config.edit` | Read or update Gemini configuration. |
| GET | `/gemini-config/reveal` | `gemini_config.edit` | Reveal stored Gemini API key; highly sensitive. |
| GET | `/notifications/pusher-config` | auth + active | Client Pusher configuration. |
| GET | `/notifications` | auth + active | Notification list. |
| GET | `/notifications/unread-count` | auth + active | Unread count. |
| PATCH | `/notifications/{id}/read` | auth + active | Mark one notification read. |
| POST | `/notifications/mark-all-read` | auth + active | Mark all notifications read. |
| DELETE | `/notifications/{id}` | auth + active | Delete one notification. |
| GET/POST | `/pusher-config` | `pusher_config.view` / `pusher_config.edit` | Read or save Pusher settings. |
| POST | `/pusher-config/test` | `pusher_config.edit` | Test Pusher connectivity. |
| GET | `/users` | `users.view` | Paginated/filterable users. |
| GET | `/users/{id}` | `users.view` | User detail. |
| POST | `/users` | `users.create` | Create user. |
| PUT | `/users/{id}` | `users.edit` | Update user. |
| PUT | `/users/restore/{id}` | `users.restore` | Restore soft-deleted user. |
| DELETE | `/users/{id}` | `users.delete` | Soft-delete user. |
| GET | `/activity-logs` | `activity_logs.view` | Filterable activity/audit log. |
| GET | `/roles`, `/roles/{id}` | `roles.manage` | List or view roles. |
| GET | `/permissions` | `roles.manage` | List defined permissions. |
| POST | `/roles` | `roles.manage` | Create role. |
| PUT | `/roles/{id}/permissions` | `roles.manage` | Replace a role's permission list. |
| DELETE | `/roles/{id}` | `roles.manage` | Delete role. |

### Core request payloads

```json
// POST /login
{ "email": "user@example.com", "password": "secret" }

// POST /users (create); password is optional on update
{
  "name": "Amina Rahman",
  "email": "amina@example.com",
  "password": "at-least-6-characters",
  "roles": ["sales_person"],
  "platform": "upwork",
  "status": "active",
  "shifts": [1],
  "categories": [2],
  "sub_categories": [5]
}

// PUT /shift-config
{
  "shift_count": 2,
  "day_start_time": "09:00",
  "day_end_time": "21:00",
  "slots": [
    { "shift_name": "Morning", "shift_start_time": "09:00", "shift_end_time": "15:00" },
    { "shift_name": "Evening", "shift_start_time": "15:00", "shift_end_time": "21:00" }
  ]
}
```

`roles` or `role` is required on user creation. Sales persons require one or more `shifts`; writers/prospect leads require `categories`. The API validates duplicate email and payment-method details. Shift slots must equal `shift_count`, stay inside the day window and not overlap.

## 4. Upwork reference

### Lookup, category, account and template

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET | `/active/categories` | auth | Active category lookup (`platform`). |
| GET | `/active/sub-categories` | auth | Active sub-category lookup (`category_id`, `platform`). |
| GET | `/categories`, `/categories/{id}` | `categories.view` | List/detail categories. |
| POST | `/categories` | `categories.create` | Create category. |
| PUT | `/categories/{id}`, `/categories/{id}/status` | `categories.edit` | Update category or its status. |
| PUT | `/categories/restore/{id}` | `categories.restore` | Restore category. |
| DELETE | `/categories/{id}` | `categories.delete` | Delete category. |
| GET | `/categories/{categoryId}/sub-categories` | auth | Children for category. |
| GET | `/sub-categories`, `/sub-categories/{id}` | `sub_categories.view` | List/detail sub-categories. |
| POST | `/sub-categories` | `sub_categories.create` | Create sub-category. |
| PUT | `/sub-categories/{id}`, `/sub-categories/{id}/status` | `sub_categories.edit` | Update sub-category/status. |
| PUT | `/sub-categories/restore/{id}` | `sub_categories.restore` | Restore sub-category. |
| DELETE | `/sub-categories/{id}` | `sub_categories.delete` | Delete sub-category. |
| GET | `/upwork-accounts/job/{jobId}` | auth | Accounts compatible with a job. |
| GET | `/upwork-accounts/active/list` | auth | Active account lookup. |
| GET | `/upwork-accounts/category/{categoryId}` | auth | Accounts matching category. |
| GET | `/upwork-accounts/sub-category/{subCategoryId}` | auth | Accounts matching sub-category. |
| GET | `/upwork-accounts`, `/upwork-accounts/{id}` | `upwork_accounts.view` | List/detail accounts. |
| POST | `/upwork-accounts` | `upwork_accounts.create` | Create account. |
| PUT | `/upwork-accounts/{id}`, `/upwork-accounts/{id}/status` | `upwork_accounts.edit` | Update account/status. |
| PUT | `/upwork-accounts/restore/{id}` | `upwork_accounts.restore` | Restore account. |
| DELETE | `/upwork-accounts/{id}` | `upwork_accounts.delete` | Delete account. |
| GET | `/active/templates` | auth | Active template lookup. |
| GET | `/templates`, `/templates/{id}` | `templates.view` | List/detail templates. |
| POST | `/templates` | `templates.create` | Create template; multipart allowed. |
| PUT | `/templates/{id}` | `templates.edit` | Update template; method override may be used with multipart. |
| PUT | `/templates/restore/{id}` | `templates.restore` | Restore template. |
| DELETE | `/templates/{id}` | `templates.delete` | Delete template. |

Category payload: `{ "platform": "upwork|fiverr", "name": "…", "task_code": "…", "status": "active|inactive" }`. Sub-category additionally requires `category_id`. Upwork account requires `account_name`, unique `account_email`, and at least one `category_ids` item when creating. Template requires `name` plus either `category_id` or non-empty `categories`; files accept PDF/DOC/DOCX and listed image types up to 25 MB each.

### Jobs and proposals

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET | `/jobs/by-url` | auth | Find job by `url`. |
| GET | `/jobs`, `/jobs/{id}`, `/jobs/slug/{slug}` | `jobs.view` **or** `proposals.view` | List/detail job. |
| POST | `/jobs/{id}/book`, `/jobs/{id}/unbook` | `jobs.view` **or** `proposals.view` | Acquire/release work lock. |
| POST | `/jobs/extract-from-image` | `jobs.import` **or** `proposals.create` | Extract job data from screenshot. |
| POST | `/jobs`, `/jobs/import` | `jobs.create` **or** `proposals.create`; `jobs.import` **or** `proposals.create` | Create/import job. |
| POST | `/jobs/sync` | `jobs.sync` **or** `proposals.sync` | Sync a job using `job_url`. |
| POST | `/jobs/{id}/retry-scrape` | `jobs.retry_scrape` **or** `proposals.retry_scrape` | Retry external scrape. |
| PUT | `/jobs/{id}`, `/jobs/{id}/job-status` | `jobs.edit` **or** `proposals.edit` | Edit job or status. |
| PUT | `/jobs/restore/{id}` | `jobs.restore` **or** `proposals.restore` | Restore job. |
| DELETE | `/jobs/{id}` | `jobs.delete` **or** `proposals.delete` | Delete job. |
| GET | `/proposals`, `/proposals/{id}` | `proposals.view` | List/detail proposals. |
| POST | `/proposals` | `proposals.create` | Create proposal(s) for selected accounts. |
| PUT | `/proposals/{id}`, `/proposals/{id}/status` | `proposals.edit` | Update proposal/status. |
| PATCH | `/proposals/{id}/submit` | `proposals.submit` | Record submission. |
| PATCH | `/proposals/{id}/accept`, `/proposals/{id}/request-transfer` | `proposals.accept` | Accept or request transfer. |
| PATCH | `/proposals/{id}/reject` | `proposals.reject` | Reject proposal with reason. |
| PATCH | `/proposals/{id}/reassign` | `proposals.reassign` | Reassign using `writer_id`. |
| POST | `/proposals/{id}/upload` | `proposals.upload_file` **or** `proposals.replace_file` | Upload files (multipart). |
| GET | `/proposals/{id}/download-all` | `proposals.download_file` | Download proposal files. |
| DELETE | `/proposals/{id}` | `proposals.delete` | Delete proposal. |

Key job fields are `title`, `description`, `job_url`, `sales_person_id`, `source_platform` (`upwork|linkedin`), `budget_type`, amount/hour, client information, `category_id`, `skills[]`, `posted_at`, `scraped_status` and `raw_data`. Import requires `title`; URLs are validated and de-duplicated. Proposal creation requires `upwork_account_ids[]`; it can include template IDs, writer IDs, source type, job/client details and `skills[]`.

### Proposal/prospect/project exports and Upwork reports

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| POST | `/proposal-exports` | `proposals.export` | Start proposal export. |
| GET | `/proposal-exports/recover/{requestKey}`, `/proposal-exports/{id}`, `/proposal-exports/{id}/download` | `proposals.export` | Recover/status/download proposal export. |
| POST | `/prospect-exports` | `prospects.export` | Start prospect export. |
| GET | `/prospect-exports/recover/{requestKey}`, `/prospect-exports/{id}`, `/prospect-exports/{id}/download` | `prospects.export` | Recover/status/download prospect export. |
| GET | `/prospects/today-counts`, `/prospects`, `/prospects/{id}` | `prospects.view` | Prospect dashboard/list/detail. |
| GET | `/leads/{leadId}/stats` | auth | Prospect-lead statistics, optional dates/category. |
| POST | `/prospects` | `prospects.create` | Create prospect. |
| POST | `/prospects/extract-delay` | `prospects.create` | Extract response delay from image. |
| PUT | `/prospects/{id}` | `prospects.edit` | Edit prospect. |
| POST | `/prospects/{id}/set-interview` | `prospects.set_interview` **or** `prospects.edit_interview` | Set interview. |
| POST | `/prospects/{id}/follow-up`, `/reactivate` | `prospects.edit` **or** `prospects.create` **or** `prospects.set_interview` | Record follow-up/reactivate. |
| POST | `/prospects/{id}/reject` | `prospects.reject` | Reject with reason. |
| POST | `/prospects/{id}/accept` | `prospects.edit` | Accept prospect. |
| POST | `/prospects/{id}/reassign` | `prospects.reassign` | Reassign to `prospect_lead_id`. |
| PUT | `/prospects/{id}/status` | `prospects.client` **or** `prospects.reject` **or** `prospects.edit` | Update status / project data. |
| PUT | `/prospects/{id}/silent-reason` | edit/create/set-interview permission | Save `silent_reason`. |
| GET | `/projects` | `projects.view` | Project list. |
| POST | `/projects` | `projects.create` | Create project. |
| PUT | `/projects/{id}`, `/projects/{id}/permanent`, `/projects/{id}/discontinue` | `projects.edit`; permanent/discontinue permissions | Update/lifecycle operations. |
| POST | `/project-exports` | `projects.export` | Start project export. |
| GET | `/project-exports/recover/{requestKey}`, `/{id}`, `/{id}/download` | `projects.export` | Recover/status/download project export. |
| GET | `/upwork-reports`, `/upwork-reports/prospects` | `admin` **or** `reports.view` **or** `reports.trend_view` | Upwork report data. |
| GET | `/upwork-reports/clients`, `/clients/export` | `admin` **or** `reports.client_view` (export also `reports.export`) | Client report/export. |
| GET | `/upwork-reports/performance`, `/performance/details`, `/performance/filters`, `/performance/export` | `admin` **or** performance/report export permissions | Performance report, details, filter metadata, export. |

Prospect creation requires `job_post_id`, `submit_proposal_id` (unique per prospect), `prospect_manager_id`, `client_name`, first-message and response timestamps, and non-negative `response_delay_minutes`. Project creation requires `project_name`, `client_name`, and either `external_job_id` or `job_id`.

### Settings and legacy resource

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET/PUT | `/upwork-settings` | settings/alert view or edit permissions | Read/update SLA, targets and report settings. |
| POST | `/upwork-settings/test-performance-alert` | target-test or settings edit | Test performance alert. |
| POST | `/upwork-settings/test-response-delay-alert` | SLA-test or settings edit | Test response-delay alert. |
| POST | `/upwork-settings/test-weekly-report` | report-test or settings edit | Test weekly-report alert. |
| GET/POST/PUT/PATCH/DELETE | `/v1/upworks`, `/v1/upworks/{upwork}` | auth | Laravel `apiResource` legacy CRUD route. No UI service currently targets it. |

## 5. Fiverr reference

### Reporting, settings, designation and payments

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET | `/fiverr-reports`, `/fiverr-reports/leads`, `/fiverr-reports/team-performance` | `fiverr_reports.view` | Fiverr reports. |
| GET/POST | `/fiverr/settings` | `fiverr_settings.view` / `.edit` | Read/update fee and follow-up settings. |
| GET | `/active/designations` | auth | Active designation lookup. |
| GET | `/designations`, `/designations/{id}` | `designations.view` | List/detail designation. |
| POST/PUT/DELETE | `/designations`, `/designations/{id}` | create/edit/delete permission | Manage designations. |
| GET | `/payment-accounts/{accountId}/payment-methods` | auth | Methods attached to payment account. |
| GET | `/payment-accounts`, `/payment-accounts/{id}` | `payment_accounts.view` | List/detail payment accounts. |
| POST | `/payment-accounts` | `payment_accounts.create` | Create payment account. |
| PUT | `/payment-accounts/{id}` | `payment_accounts.edit` | Update account. |
| PATCH | `/payment-accounts/{id}/status/{status}` | `payment_accounts.edit` | Change account status. |
| POST | `/payment-accounts/{id}/restore` | `payment_accounts.restore` | Restore account. |
| DELETE | `/payment-accounts/{id}` | `payment_accounts.delete` | Delete account. |
| GET | `/payment-methods`, `/payment-methods/{id}` | `payment_methods.view` | List/detail payment methods. |
| POST/PUT/DELETE | `/payment-methods`, `/payment-methods/{id}` | create/edit/delete permission | Manage payment methods. |
| PATCH | `/payment-methods/{id}/status/{status}` | `payment_methods.edit` | Change method status. |
| POST | `/payment-methods/{id}/restore` | `payment_methods.restore` | Restore method. |

### Account and gig

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET | `/fiverr/accounts/active-list` | auth | Active account lookup. |
| POST | `/fiverr/accounts/upload-image` | auth | Upload account image (`image`, optional `folder`). |
| GET | `/fiverr/accounts`, `/fiverr/accounts/{id}` | `fiverr_accounts.view` **or** `fiverr_gigs.view` | List/detail Fiverr account. |
| POST | `/fiverr/accounts` | account-create **or** gig-create permission | Create account. |
| PUT | `/fiverr/accounts/{id}` | account-edit **or** gig-edit permission | Update account. |
| PATCH | `/fiverr/accounts/{id}/status/{status}` | account-edit **or** gig-edit permission | Update account status. |
| PATCH | `/fiverr/accounts/{id}/restore` | `fiverr_accounts.restore` | Restore account. |
| DELETE | `/fiverr/accounts/{id}` | account-delete **or** gig-delete permission | Delete account. |
| GET | `/fiverr/accounts/{accountId}/active-gigs`, `/{accountId}/gigs` | auth; view permissions for all gigs | Active/list gigs. |
| POST | `/fiverr/accounts/{accountId}/gigs` | account-create **or** gig-create permission | Create gig. |
| PUT/DELETE | `/fiverr/accounts/gigs/{id}` | account-edit/gig-edit; account-delete/gig-delete | Update/delete gig. |

### Leads and orders

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET | `/fiverr-orders`, `/leads` | `fiverr_orders.view` | Order/lead lists. |
| GET | `/leads/{id}` | `fiverr_orders.view_details` | Lead detail. |
| POST | `/fiverr-orders` | `fiverr_orders.create` | Create order. |
| POST | `/fiverr-orders/extract-from-image` | `fiverr_orders.create` | Extract order from image (multipart `image`). |
| POST | `/leads/extract`, `/leads/extract-from-image` | `fiverr_orders.create` | Extract lead from image(s). |
| POST | `/leads/{id}/offer` | `fiverr_orders.add_offer` | Save custom offer. |
| POST | `/leads/{id}/accept-offer` | `fiverr_orders.accept_offer` | Record accepted offer/contact. |
| POST | `/leads/{id}/assign-team` | `fiverr_orders.assign_team` | Handover/assign operation team. |
| POST | `/leads/{id}/contact` | assign-team **or** accept-offer permission | Save contact info. |
| POST | `/leads/{id}/delivery` | `fiverr_orders.mark_delivered` | Record delivery. |
| POST | `/leads/{id}/completion` | `fiverr_orders.mark_completed` | Record completion. |
| POST | `/leads/{id}/rating` | `fiverr_orders.add_rating` | Save client rating/review. |
| POST | `/leads/{id}/mark-replied` | follow-up/create/view permission | Mark reply and optional evidence. |
| POST | `/leads/{id}/follow-up` | `fiverr_orders.follow_up` | Record follow-up. |
| POST | `/leads/{id}/reactivate` | `fiverr_orders.reactivate` | Reactivate lead. |
| POST | `/leads/{id}/reject` | `fiverr_orders.reject` | Reject (`reason`, optional `stage`). |
| POST | `/leads/{id}/confirm-rejection`, `/retrieve-rejection` | `fiverr_orders.verify_reject` | Confirm/retrieve rejection. |

Image extraction accepts specified image MIME types and enforces 25 MB per image in the stage extractor. Consumers should submit `multipart/form-data` and validate the AI-extracted fields before applying a lifecycle action.

### Forecast

| Method | Path | Access | Purpose |
| --- | --- | --- | --- |
| GET | `/fiverr/forecasts` | `fiverr_forecast.view` | Forecast matrix. |
| GET | `/fiverr/forecasts/overall-targets`, `/overall-target-by-month`, `/validate-account` | `fiverr_forecast.view` | Target and account validation lookup. |
| POST | `/fiverr/forecasts/overall-targets` | `fiverr_forecast.manage_overall` | Create monthly overall target. |
| PUT/DELETE | `/fiverr/forecasts/overall-targets/{id}` | `fiverr_forecast.manage_overall` | Update/delete target. |
| PATCH | `/fiverr/forecasts/overall-targets/{id}/status`, `/{id}/verify` | `fiverr_forecast.manage_overall` | Update/verify target. |
| POST | `/fiverr/forecasts/targets` | `fiverr_forecast.create` **or** `.edit` | Save account/gig targets. |
| POST | `/fiverr/forecasts/verify` | `fiverr_forecast.verify` | Verify a target. |
| PATCH | `/fiverr/forecasts/account-target-status`, `/toggle-status` | `fiverr_forecast.change_status` | Change target state. |
| POST | `/fiverr/forecasts/daily-entry` | `fiverr_forecast.daily_entry` | Save daily actual entry. |
| POST | `/fiverr/forecasts/toggle-review` | `fiverr_forecast.review` | Update daily review. |
| POST | `/fiverr/forecasts/seed-defaults` | `roles.manage` **or** `fiverr_forecast.manage_overall` | Seed defaults; administrative use only. |

```json
// POST /fiverr/forecasts/overall-targets
{ "year": 2026, "month": 10, "budget_target": 25000, "order_count_target": 20, "status": "active", "notes": "…" }

// POST /fiverr/forecasts/targets
{
  "year": 2026,
  "month": 10,
  "targets": [{ "fiverr_account_id": 4, "fiverr_gig_id": 12, "budget_target": 5000, "order_count_target": 4 }]
}

// POST /fiverr/forecasts/daily-entry
{ "fiverr_account_id": 4, "fiverr_gig_id": 12, "entry_date": "2026-10-19", "amount": 350, "order_count": 1, "notes": "…" }
```

## 6. Files, dates, filters and exports

- API dates are generally ISO-style (`YYYY-MM-DD`); times in shift config are `HH:MM` (24-hour). Individual legacy fields may accept strings—follow the endpoint validator/UI behaviour.
- List endpoints commonly accept pagination, search, status, platform and date range filters. Filter names are not standardized globally; inspect the corresponding frontend service/page before building a new integration.
- File upload endpoints require `multipart/form-data`. Do not set a manual boundary header when your HTTP client creates it automatically.
- Export endpoints are asynchronous request/status/download flows. Preserve the returned request key or export ID; call `recover` after a client retry rather than starting duplicate exports.

## 7. Example integration

```bash
BASE_URL='https://pms.example.com/api'
TOKEN='replace-with-sanctum-token'

curl --request GET "$BASE_URL/jobs?status=proposal_pending" \
  --header "Accept: application/json" \
  --header "Authorization: Bearer $TOKEN"
```

For a browser-based app, the deployed Nginx configuration normally makes `/api` same-origin. Do not move the bearer token into a URL, local log, analytics event or exported support screenshot.

## 8. Development operations

The repository is a Docker Compose monorepo. Primary commands from repository root:

```bash
make init       # create backend .env from the example
make dev        # build/start development stack
make dev-d      # same, detached
make logs       # tail logs
make migrate    # run migrations
make down       # stop services
```

The published development port defaults to `8012`. Backend env lives at `apps/backend/.env`; it is intentionally ignored by Git. Production-like startup uses `make prod-build` then `make prod-up`.

## 9. Known implementation gaps

These observations were verified from source, not inferred from the UI.

1. **Public registration plus inconsistent active-user enforcement is an access-control risk.** `POST /register` creates a disabled user but returns a bearer token immediately. Core protected routes include `active.user`; Upwork and Fiverr module route groups only declare `auth:sanctum`. A newly registered/disabled account can therefore still reach module routes that merely require a token. Disable public registration for this internal system (or require invitation/email verification), add `active.user` at each module's outer route group, revoke issued tokens on disable, and add automated coverage.
2. **Public-looking lookup/upload routes have broad authorization.** Several module lookups and `POST /fiverr/accounts/upload-image` require only a token, not a feature permission. Define explicit least-privilege permissions, especially for uploads and account/payment-adjacent lookups.
3. **Frontend/backend contract drift exists.** The frontend service contains calls to `/proposals/scrape`, `/proposals/{id}/assign`, and `/active/templates/{id}`, but the current route files do not expose those endpoints. Remove/replace stale calls, or add and test the intended backend endpoints. Template update also uses POST with `_method=PUT`, while the registered route is PUT; verify this through an integration test.
4. **Login/reset endpoints have no route-level rate limiter.** Add a named throttle policy for login, password reset and image/AI extraction, with user/IP-aware logging and a consistent generic reset response to prevent enumeration and brute force.
5. **Route ordering is fragile.** Dynamic routes such as `/jobs/{id}` are registered before `/jobs/slug/{slug}`; static/typed route constraints should be added and routes ordered from most specific to least specific to prevent collisions.
6. **No CI workflow is committed and automated coverage is narrow.** The repository has only a small number of visible tests; the root PHPUnit configuration includes `app/` but not module source directories. Add CI for backend tests, frontend lint/build and security dependency audit; ensure module tests/source are covered.
7. **Current frontend lint command is not reproducible in this checkout.** `npm run lint` invoked an installed ESLint 6.4.0, which cannot read the committed flat ESLint config intended for ESLint 10. Reinstall dependencies from the lockfile in a clean environment and enforce `npm ci && npm run lint` in CI.
8. **Secrets are present in the committed backend environment example.** Treat all such values as exposed: revoke/rotate them in their providers, remove concrete credential values from the example, and inject secrets only through deployment secret management. This document deliberately does not reproduce them. Also use production-safe `APP_DEBUG`, log level and CORS allow-list values rather than copying development defaults.
9. **External HTTP and AI/OCR operations need explicit resilience controls.** Add request timeouts, retries with backoff, queueing, idempotency keys, provider error mapping, rate limits and cost/usage telemetry for scrape/extract/export work.

