Skip to content

Authentication

The AppGantry API authenticates every request through one of four mechanisms, picked by route:

Mechanism For Carried as
Access JWT Developers in the web app or programmatic clients Authorization: Bearer <jwt> or dfdeveloper cookie
Project access token CI and other automation Authorization: Bearer ag_prj_...
Personal access token Individual scripts, SDKs, interactive tooling Authorization: Bearer ag_pat_...
Invite token Testers installing builds invite_token in the URL path

The web app sets cookies and uses JWTs; CI uses project access tokens; personal access tokens cover individual scripting; testers use single-use install links.

Login

POST /api/v1/login/email
Content-Type: application/json

{"email": "alice@example.com", "password": "..."}

On success the body returns both tokens for programmatic clients:

{
  "token": "<access JWT>",
  "refresh_token": "<refresh token>"
}

…and the same two values are set as cookies for the web app:

Cookie Lifetime Scope
dfdeveloper TOKEN_LIFETIME_IN_SECONDS + 10 (≈10 minutes) Whole API
dfdeveloper_refresh Long-lived Path-scoped to /api/v1/login

Both cookies are HttpOnly, SameSite=Lax, and Secure outside local dev.

Access JWT

Property Value
Algorithm RS256
Issuer (iss) appgantry (prod) or appgantry-local (dev)
Audience (aud) appgantry-api
Lifetime 10 minutes
Required claims sub, email, iss, aud, iat, exp

A token missing any required claim is rejected at verification (401), not silently defaulted.

Refresh

POST /api/v1/login/refresh
Cookie: dfdeveloper_refresh=<refresh_token>

…or, for programmatic clients:

POST /api/v1/login/refresh
Content-Type: application/json

{"refresh_token": "..."}

Returns a fresh {token, refresh_token} pair. Refresh tokens are single-use. The returned refresh_token replaces the one you used.

Logout

POST /api/v1/login/logout
Authorization: Bearer <jwt>

Logout sets the developer's min-issued-at to now. Every JWT issued before that moment is rejected by every subsequent verify_identity call. There's no per-token revocation list, just a single per-developer floor. This means a logout instantly invalidates all outstanding sessions for that developer.

Project access tokens

Project access tokens are the credential for CI and other automation. They are bound to a single project (and its organization), carry a project role, and are owned by the project rather than by any developer, so they outlive personnel changes.

Property Value
Format ag_prj_<random>
Bound to One project + its organization
Grants A project role, capped at the creator's own role
Lifetime Until revoked (or the expiry you set at creation)
Use in CI, build pipelines, automation

Create, rotate, and revoke project access tokens from a project's Access tokens page in the web app (see CI with project access tokens).

Personal access tokens

Personal access tokens are bound to a developer and an organization, carry a fixed set of scopes, and survive password rotations. They suit individual scripting and interactive tooling; for CI, prefer a project access token.

Property Value
Format ag_pat_<random>
Bound to One developer + one organization
Lifetime Until revoked (or the expiry you set at creation)
Use in Individual scripts, SDKs, interactive tooling

Create and revoke PATs from Personal access tokens in the web app.

Where long-lived tokens aren't accepted

Both project and personal access tokens are deliberately rejected on a few surfaces:

  • BYOSA management routes: setup, diagnose, teardown require an interactive login (a fresh JWT). Long-lived tokens are too broad for the blast radius BYOSA management carries.
  • Account-level routes: password change, email change, MFA enrollment require a JWT.
  • Token management routes: creating, rotating, or revoking project access tokens requires an interactive developer session, so a project access token cannot manage other tokens.

These return HTTP 403 with error: "operation_not_allowed" and a message explaining which credential is expected.

Email verification

New accounts must verify their email before they can do anything state-changing. While unverified, the API returns:

{
  "error": "email_not_verified",
  "message": "Verify your email before performing this action."
}

…with HTTP 403. Read-only routes (list orgs you're a member of, see your own profile) still work.

Resend the verification email with:

POST /api/v1/developers/resend-verification
Content-Type: application/json

{"email": "alice@example.com"}

The verification landing page is served at:

GET /api/v1/developers/{developer_id}/verify_email?token=...

The token in the email link is one-time and time-bound.

Password reset

Two routes:

POST /api/v1/developers/reset_password_request
Content-Type: application/json

{"email": "alice@example.com"}

The response is always 204, regardless of whether the email is on file - this prevents account enumeration. If the email is registered, a reset email goes out.

POST /api/v1/developers/reset_password
Content-Type: application/json

{"token": "...", "new_password": "..."}

The reset token in the email is one-time and time-bound.

Anti-enumeration policy

Login, password reset, and email verification deliberately return identical responses whether the email exists or not. This is a trade-off. A real user with a typo'd email sees the same success message a probing attacker would, but it removes the account-enumeration oracle attackers use to build target lists.

The web app's UX mirrors this: every signup/login/reset success message is identical regardless of outcome.

Invite tokens (testers)

Testers don't log in. They receive a link of the form:

https://app.appgantry.com/install/{invite_token}

The invite token authenticates the request, scopes it to a single build (or single tester record across builds), and is one-time-use for state-changing actions. The install page renders an OS-appropriate install button; tapping it streams the build through a signed download URL.

See also