# Add API Rate Limiting to a Laravel App from a Linear Issue

Yes. An agent picks up the Linear issue, adds API rate limits to your Laravel routes, tests them with PHPUnit and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-api-rate-limit/laravel/), in a Project that has your repository.

## What the agent does

The agent starts from the issue text and the repository, cloned into its own cloud sandbox, and reads before it writes, because a Laravel app often throttles its API already in a way nobody remembers. In a codebase like BookStackApp/BookStack (Laravel `^v13.35.0` on PHP `^8.3.0`), the `api` middleware group in `app/Http/Kernel.php` already runs `ThrottleApiRequests`, a subclass of Laravel's `ThrottleRequests` whose `resolveMaxAttempts` returns `config('api.requests_per_minute')`. That value comes from `app/Config/api.php` as `env('API_REQUESTS_PER_MIN', 180)`, and `.env.example.complete` documents `API_REQUESTS_PER_MIN=180`. So two of the three things the issue asks for, a 180 per minute limit and a value set in `.env`, are already true, and the agent says so instead of adding a second throttle on top.

Then it looks for the gap between the issue and the code. The issue says the limit is per API token; the comment in `app/Config/api.php` says "per minute by a single user", and one BookStack user can hold several tokens. `app/App/Providers/RouteServiceProvider.php` also defines `RateLimiter::for('api', ...)` at `Limit::perMinute(60)` keyed by user id or IP, but no route names `throttle:api` (only `throttle:public` in `routes/web.php` and `throttle:exports` in the export controllers), so that 60 per minute limiter never runs and reads like the real one. The agent writes these findings into its plan: key the API limit by the token the request authenticated with, using a named `RateLimiter` limiter that reads `api.requests_per_minute`, and remove or reconcile the unused definition so there is one place the API limit lives.

It then makes the change in the files that own it: the limiter in `RouteServiceProvider.php`, the middleware entry in the `api` group, the `api.php` config comment and the `.env.example.complete` line if the meaning changes. It keeps the headers Laravel's throttle already sends, which BookStack's tests pin down: `tests/Api/ApiAuthTest.php` asserts `x-ratelimit-limit` and `x-ratelimit-remaining` on every request and a `429` with `retry-after` and a JSON `error.code` of 429 once the limit is hit, and `tests/Api/ApiConfigTest.php` changes `api.requests_per_minute` at runtime and expects the header to follow. The agent adds tests beside those: two tokens for one user each get their own count, and a lower configured limit still answers 429 with the same JSON shape.

Last, it runs the suite the way the project's own CI does. BookStack's `phpunit.xml` sets `DB_CONNECTION` to `mysql_testing`, which `app/Config/database.php` points at a `bookstack-test` database on `127.0.0.1`, and the workflow in `.forgejo/workflows/test-php.yml` creates that database and user, runs `artisan migrate --force --database=mysql_testing`, seeds `DummyContentSeeder` and then runs `./vendor/bin/phpunit`. The agent starts MySQL in the sandbox, repeats those steps, runs `composer test`, fixes what fails and runs it again before it pushes a branch.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add API Rate Limiting to a Laravel App. Limit API tokens to 180 requests per minute with Laravel's RateLimiter, make the limit configurable in .env, and return the standard rate limit headers.\" Before changing anything, find how this app throttles API requests today (the api middleware group, any ThrottleRequests subclass, every RateLimiter::for definition and the config value it reads) and say what already matches the issue. Change only what is missing, keep one place where the API limit is defined, and keep the X-RateLimit-Limit, X-RateLimit-Remaining and Retry-After headers and the existing 429 response body. Add PHPUnit tests that hit the limit and check the headers, start MySQL in the sandbox, create and migrate the test database the way the project's CI does, and run composer test or vendor/bin/phpunit until it passes. Open a pull request that links back to the issue and lists what was already in place, what changed and why.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch whose description starts with what the agent found: in an app shaped like BookStack, that the API was already limited to 180 requests a minute through `API_REQUESTS_PER_MIN`, that the limit counted per user rather than per token, and that the 60 per minute `api` limiter in `RouteServiceProvider.php` was never applied. The diff then touches only the files that own the limit and the new tests in `tests/Api/`, and the description links back to the Linear issue so the issue shows the work.

The proof is the test run. The session's transcript keeps every command with its output: the MySQL start, the migrate and seed against `mysql_testing`, and each `composer test` run, including any that failed before the fix. If the repository runs its PHP tests on GitHub Actions, those checks run on the pull request too; BookStack's own test workflow lives under `.forgejo/` and does not run on GitHub, so for a repository like it the transcript is where the passing suite is shown.

Before merging, a reviewer checks that the limit is keyed by what the team actually means by a client (a token, a user or an IP), that no second throttle still runs in front of the API, that the new tests fail when the limiter is removed, and that the documented `.env` value and its default agree.

## Run it on every Linear issue labelled agent

In the console, an automation with a `linear` trigger on `linear.issue.labeled` and the label `agent` starts one run per labelled issue, with the issue as its context, in the Project's repository. Changes made by an app integration never fire it, so the agent's own updates do not start new runs.

From your own server, the same run is one `POST /o/{org}/sessions` per issue: put the repository in `resources`, and add the issue's `https://linear.app/<workspace>/issue/<ID>` URL there too to have Rush read it through your Linear connection into the prompt. A webhook on the API key posts `run.completed` or `run.failed` when the turn finishes, with the `session_url` to read the result.

## Why run it on Rush?

Testing a rate limit means running the app's real suite against a real database, and for a Laravel project that is MySQL with a migrated and seeded test schema. In a Rush sandbox the agent installs Composer dependencies, starts MySQL and creates `bookstack-test` or whatever your `phpunit.xml` names on its own disk, so nobody's laptop database is migrated or reset, and the tests that hammer the API until it answers 429 run where they cannot slow anything else down.

A label in Linear becomes a reviewable change with its evidence attached: the transcript shows the agent reading the existing throttle before touching it and every PHPUnit run with its output, and the pull request lists what it left alone. Several labelled issues can run at once, each in its own sandbox with its own branch, up to the key's `max_concurrent` limit.

## Frequently asked questions

### My app already has some throttling. Will it add a second limiter?

The prompt tells it to find the existing throttle first: the `api` middleware group, any `ThrottleRequests` subclass and every `RateLimiter::for` definition. It changes what is there and keeps one place where the API limit is defined. If it finds a limiter that is defined but never applied, it says so in the pull request.

### Does it run the tests against MySQL or SQLite?

Against whatever your `phpunit.xml` and database config name. For a project like BookStack that is the `mysql_testing` connection, so the agent starts MySQL in the sandbox, creates the test database and user, and migrates and seeds it the way the project's CI workflow does before running PHPUnit.

### Which rate limit headers does the response keep?

Laravel's throttle middleware sends `X-RateLimit-Limit` and `X-RateLimit-Remaining` on each response and `Retry-After` on a 429. The prompt asks the agent to keep those and the existing 429 body, and to add tests that assert them, so a client that already reads them keeps working.

### What if the issue is wrong about the current state?

The agent reports what it found rather than doing the issue literally. If the limit is already 180 and already set from `.env`, the pull request says so and changes only what is missing, such as counting per token instead of per user.
