# Add JWT Authentication to a Vapor App from a Linear Issue

Yes. An agent reads the issue, adds a User model, login route and JWT bearer middleware to your Vapor app, writes Swift Testing tests and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-jwt-auth/vapor/), in a Project that has your repository.

## What the agent does

The agent starts from the Linear issue and the repository in its own cloud sandbox, and reads how the app is put together before it adds authentication. In a project generated from vapor/template with Fluent, `Package.swift` is `swift-tools-version:6.3`, depends on `vapor` from 4.121.4, `fluent` from 4.13.0, a Fluent driver (the template offers Postgres 2.12.0, MySQL 4.8.0 and SQLite 4.9.0) and `swift-nio` from 2.101.0, and turns on upcoming features such as `ExistentialAny`, `InternalImportsByDefault` and `MemberImportVisibility`, which decide how new files must write their imports and protocol types. `Sources/App/routes.swift` registers `GET /`, `GET /hello` and `TodoController`; `Controllers/TodoController.swift` groups `todos` with `GET`, `POST` and `DELETE /todos/:todoID`; `Models/Todo.swift` is a `Model` with a `UUID` id and a `title`; `Migrations/CreateTodo.swift` creates the `todos` schema; and `configure.swift` reads `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` and `DATABASE_NAME` from the environment, adds the migration and calls `routes(app)`. Nothing in the app knows what a user is.

The agent adds the `vapor/jwt` package to `Package.swift` and a `JWT` product to the app target, then builds the pieces in the repository's own shape. `Models/User.swift` is a Fluent model with a unique `email` and a `passwordHash`, with `Migrations/CreateUser.swift` beside `CreateTodo` and `DTOs/UserDTO.swift` beside `TodoDTO`. `Controllers/AuthController.swift` adds `POST /auth/register`, which hashes the password with `req.password.async.hash`, and `POST /auth/login`, which verifies it and returns a token signed from a `SessionToken` payload carrying the user id as `sub` and an `exp` a configurable number of hours out; its `verify` method calls `exp.verifyNotExpired()`. In `configure.swift` the agent adds the signing key from a `JWT_SECRET` environment variable with `app.jwt.keys.add(hmac:digestAlgorithm:)` and fails startup when it is missing outside the testing environment, so a deployment never runs unsigned. `TodoController` keeps its routes but its group gains `SessionToken.authenticator()` and `guardMiddleware()`, so `GET /todos` without a bearer token answers `401` and with one reads the user from `req.auth.require(SessionToken.self)`.

Tests follow `Tests/AppTests/AppTests.swift`, which already uses Swift Testing: a `@Suite("App Tests with DB", .serialized)` with a `withApp` helper that calls `Application.make(.testing)`, `configure`, `autoMigrate`, runs the test and `autoRevert`s, and `@Test` functions that drive `app.testing().test(.GET, "todos")` and decode `[TodoDTO]`. The agent adds tests in the same file or a sibling: register then login returns a token; `GET /todos` without a header is `401`; with the header it is `200` and lists the todos created on `app.db`; a token signed with another key is `401`; and an expired payload is refused. The `withApp` helper sets a test signing key through the environment so `configure` takes the same path it takes in production.

Verification is what `.github/workflows/test-template.yml` does for the template itself: generate the project with the toolbox for every Fluent and Leaf combination in `swift:6.3-noble`, with `mysql` and `psql` services, and run `swift test --package-path template-test` with `DATABASE_HOST` pointing at the service. For a generated app the agent runs `swift build` and `swift test` in the same image against the database Compose defines, and then `docker compose build`, since the template's `Dockerfile` builds a static release binary with `swift build -c release --static-swift-stdlib` and a new package dependency has to survive that path too.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Add JWT authentication to this Vapor app as the attached Linear issue describes. Read Package.swift, Sources/App/configure.swift, routes.swift, entrypoint.swift, Controllers/TodoController.swift, Models/Todo.swift, DTOs/TodoDTO.swift, Migrations/CreateTodo.swift, Tests/AppTests/AppTests.swift, Dockerfile and docker-compose.yml. Add the vapor/jwt package, a User model with a unique email and a password hash plus its migration and DTO, an AuthController with register and login routes where login returns a token signed from a payload with sub and exp, a signing key read from JWT_SECRET in configure.swift that fails startup when missing outside testing, and the token authenticator with guardMiddleware on the routes the issue names. Write Swift Testing tests in the existing suite style covering register and login, 401 without a token, 200 with one, a token under a wrong key and an expired payload. Run swift build, swift test against the Compose database in the swift:6.3-noble image, and docker compose build. Open a pull request that links the issue, lists the protected routes and shows the test output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    },
    {
      "url": "https://linear.app/your-org/issue/ENG-123"
    }
  ]
}
```

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 that changes `Package.swift` and `Package.resolved`, adds `Models/User.swift`, `Migrations/CreateUser.swift`, `DTOs/UserDTO.swift` and `Controllers/AuthController.swift`, changes `configure.swift` and `TodoController.swift`, and extends the tests under `Tests/AppTests/`. The description links the Linear issue, lists the routes now behind the bearer check and the two that are not, names the `JWT_SECRET` variable the deployment must set, and quotes the `swift test` summary.

The transcript at `GET /o/{org}/sessions/{id}/messages` keeps the package resolution, the build, every test run with its output and the Docker build, and `GET /o/{org}/p/{project}/sessions/{id}/changes` lists each changed file with its diff. A test that fails ends the run `needs_review` rather than as a pull request that claims to be green.

Before merging, a reviewer checks the token lifetime against the issue, confirms `JWT_SECRET` is set in each environment's secrets before the deploy that includes this change, and decides whether `register` should stay open or be restricted. A follow-up on the same session can add refresh tokens or move the key to a rotated key set.

## Run it on every Linear issue labelled agent

In the console, add an automation to the Project with this prompt and a `linear` trigger on `linear.issue.labeled`, filtered to the `agent` label and the backend team. When someone labels an issue such as "todos API needs auth before the mobile app ships", the trigger puts the issue after the prompt and the run scopes itself to the routes it names. Only label changes made by a person in your Rush org fire the trigger; changes made by an app integration never do, and the automation starts at most `max_runs_per_hour` runs in an hour.

From your own server, the same body goes to `POST /o/{org}/sessions` with the repository in `resources` and the issue's URL beside it, read through your Linear connection, with a `request_id` made from the issue id so a retried call returns the run already started instead of opening a second pull request.

## Why run it on Rush?

Authentication added without tests is authentication that fails open somewhere: a route left outside the group, a key that is empty in production, an expiry that is never checked. In its own sandbox the agent builds the app in the same `swift:6.3-noble` image the template's workflow uses, runs the suite against a real database, and keeps the output, so the pull request shows each protected route answering `401` and `200` rather than asserting that it does.

A Swift build takes minutes and a laptop, and this one happens on neither. Several issues can run at once, each in its own sandbox with its own database, and the transcript keeps every command, so a reviewer reads what ran instead of trusting a summary.

## Frequently asked questions

### Which routes get protected?

The ones the Linear issue names. The agent applies `SessionToken.authenticator()` and `guardMiddleware()` to those route groups and leaves `GET /`, `GET /hello`, `register` and `login` open, and the pull request lists both sets so a reviewer can see nothing was missed.

### Where does the signing key come from?

From a `JWT_SECRET` environment variable read in `configure.swift`, the same way the template reads `DATABASE_HOST` and the others. Outside the `.testing` environment a missing value stops startup with a clear error, so an unsigned deployment cannot happen quietly; the tests set a key through the environment so `configure` takes the same path.

### Does it work with the template's Swift 6 settings?

Yes, and the build is the proof. `Package.swift` enables `ExistentialAny`, `InternalImportsByDefault` and `MemberImportVisibility`, so the new files write `any` on protocol types and import what they use explicitly. `swift build` and `swift test` in the `swift:6.3-noble` image fail on any slip, and the pull request opens only when they pass.

### Will the Docker image still build?

The template's `Dockerfile` resolves packages, then builds a static release binary with jemalloc. A new dependency changes that path, so the agent runs `docker compose build` after the tests and includes the result; a build that fails ends the run `needs_review`.
