Reference

Authentication

This page is about your app's end users — the people signing into the product you build on Aaly. Connecting an AI coding agent to the platform itself is a separate flow; see Connect an AI agent.

Sign up

POST /<your-slug>/<project>/auth/signup
{ "name": "Ada", "password": "...", "email": "ada@example.com" }

name and password are required; at least one of email, phone, or username identifies the account. Returns 201 with:

{ "jwt": "...", "refreshToken": "...", "expiresIn": 3600,
  "user": { "id": "USR1", "name": "Ada", "tenantId": "TNT1", "email": "ada@example.com", "emailVerified": false } }

Tenant behavior on signup depends on the project's publicSignup and multiTenant flags — see Multi-tenancy for the full model. In short: signing up without a tenantId only creates a new tenant at all when the project's publicSignup is true; signing up with an existing tenantId joins that tenant and shares its data with every other user in it. tenantName only names a tenant you're creating — it's ignored when joining an existing one via tenantId.

Sign in

POST /<your-slug>/<project>/auth/signin
{ "identifier": "ada@example.com", "password": "..." }

identifier is an email, phone, or username. Returns the same { jwt, user } shape as signup — unless the identifier belongs to more than one tenant and no tenantId was passed, in which case you get a tenant-selection response instead:

{ "requiresTenantSelection": true, "tenants": [{ "tenantId": "TNT1", "tenantName": "Acme" }] }

Re-send the request with the chosen tenantId to complete sign-in.

Google sign-in

POST /<your-slug>/<project>/auth/google
{ "idToken": "<Google OIDC ID token from your client-side sign-in>" }

Verifies the token against Google, then signs in an existing user matched by email, or registers a new one (auto-creating a tenant). Same tenant-selection response as sign-in if the email matches more than one tenant.

Tokens

  • JWT — returned by signup/signin/Google, HS256-signed, valid for one hour (expiresIn: 3600). Send as Authorization: Bearer <jwt>.
  • Refresh token — returned alongside the JWT. It rotates: every call to POST /auth/refresh returns a fresh JWT and a new refresh token, and the one you sent stops working. Store both, overwrite on every refresh, and never fire two refreshes at once. Replaying an already-used refresh token revokes the whole session. POST /auth/logout revokes one session.
  • API key — aaly_<id>_<secret>, for CI, scripts, and server-to-server calls that shouldn't hold a short-lived user token:
POST /<your-slug>/<project>/auth/keys
Authorization: Bearer <jwt>

Returns the full token once:

{ "id": "KEY1", "token": "aaly_KEY1_xxxxxxxxxxxxxxxx", "createdAt": "2026-08-24T10:00:00.000Z" }

GET /auth/keys lists key metadata (never the secret again); DELETE /auth/keys/{id} revokes one. A JWT and an API key are accepted identically on every endpoint — nothing is JWT-only.

Email verification

Each project chooses how much verification it wants with the emailVerification project setting (set it with the update_project MCP tool):

ModeVerification email on signupSign-in before verifying
off (default)not sentallowed
optionalsentallowed, emailVerified is tracked
requiredsentrefused with 403 EMAIL_NOT_VERIFIED

The email carries both a link and a 6-digit code, valid for 24 hours. The link opens a confirmation page served by the API itself, so a web app builds nothing. Use the code where a link can't open your app:

POST /<your-slug>/<project>/auth/verify
{ "identifier": "ada@example.com", "code": "482913" }

POST /auth/verify-send resends it (once a minute, five times an hour). With required, signup returns { user, verificationRequired: true } and no session: show a "check your email" screen. Users who signed in with Google are already verified, and a phone-only or username-only user is never gated.

Switching a live project to required locks out every existing user who has never verified, so raise it before flipping the setting.

Forgot and reset password

POST /<your-slug>/<project>/auth/forgot-password
{ "identifier": "ada@example.com" }

POST /<your-slug>/<project>/auth/reset-password
{ "identifier": "ada@example.com", "code": "482913", "newPassword": "..." }

The code is 6 digits, valid for 15 minutes, and works once; five wrong guesses burn it. forgot-password answers identically whether or not the account exists. A successful reset signs out every existing session and emails the owner a "your password was changed" notice. There is no hosted reset page: your app collects the code.

To change a password while signed in, POST /auth/change-password with currentPassword and newPassword. It signs out the user's other sessions and returns a fresh session to replace the stored tokens.

These emails send from no-reply@aaly.io by default, branded with your project's name, plus a logo and colour if you set logoUrl and brandColor. To send from your own domain, configure an SMTP channel: see Notifications.

What's not here yet

No MFA, no on-demand "sign out everywhere" (a password reset or change does sign out other sessions; otherwise they revoke one at a time), no change-email flow (email, phone and username are immutable once set), and no RBAC. See Limits.