All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.

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.