Finish specs

This commit is contained in:
Konstantin Wohlwend 2026-01-19 10:44:26 -08:00
parent 493f6e71aa
commit e5f80ce394
14 changed files with 553 additions and 78 deletions

View File

@ -2,6 +2,8 @@
This folder contains the specification for Stack Auth's SDKs.
When writing this specification, try to write imperative pseudocode as much as possible (be explicit about what things are named, etc.).
## Notation
The spec files use the following notation:
@ -30,3 +32,31 @@ The languages should adapt:
- **Parameter conventions**: Objects vs. kwargs, etc.
- **Framework hooks**: Eg. for React, add `use*` equivalents to `get*`/`list*` methods
- **Everything else, wherever it makes sense**: Every language is unique and the patterns will differ. If you have to decide between what's idiomatic in a language vs. what was done in the Stack Auth SDK for other languages, use the idiomatic pattern.
## Implementation Notes
### Object Construction
When constructing SDK objects (User, Team, etc.) from API responses:
1. Map naming conventions to your language's naming convention
2. Objects should hold a reference to the SDK client for making API calls
3. Objects can be mutable or immutable based on language conventions
4. `update()` methods should update local properties after successful API call
### Caching
Normal functions should not cache. Some frameworks, like React, have hooks that require caching; for these, require explicit guidance.
### Pagination
Most `list*` methods support pagination:
- Request with `cursor` and `limit` query params
- Response includes `pagination: { next_cursor?: string }`
- `next_cursor` is null or absent when no more pages
- Default limit is typically 100
- Note that not all backend APIs support pagination, and some just return all items at once.
### Date/Time Formats
- API uses milliseconds since epoch for timestamps (e.g., `signed_up_at_millis`)
- Convert to your language's native Date/DateTime type

View File

@ -48,7 +48,11 @@ On 401 response with code="invalid_access_token":
### Token Refresh
Use OAuth2 refresh_token grant to get new access token:
Use OAuth2 refresh_token grant to get new access token.
Concurrency: Token refresh must be serialized. Only one refresh request should be in-flight at a time.
If a refresh is already in progress, wait for it to complete rather than starting another.
Use a mutex/lock to ensure this (or, if preferred in that framework, some kind of asynchronous mechanism that doesn't block the main thread).
POST /api/v1/auth/oauth/token
Content-Type: application/x-www-form-urlencoded
@ -62,7 +66,8 @@ Body (form-encoded):
Response on success:
{ access_token: string, refresh_token?: string, ... }
On error (e.g., refresh_token_error): clear tokens, user is signed out.
On success: store new access_token. If refresh_token is returned, store it too.
On error (e.g., refresh_token_error): clear all tokens, user is signed out.
Use an OAuth library (e.g., oauth4webapi) for proper OAuth2 handling.
@ -140,6 +145,19 @@ Store access_token and refresh_token. The tokenStore constructor option determin
Many functions also accept a tokenStore parameter to override storage for that call.
### TokenStoreInit Type
TokenStoreInit is a union type representing the different ways to provide token storage:
```
TokenStoreInit =
| "cookie" // [JS-ONLY] Browser cookies
| "memory" // In-memory storage
| { accessToken: string, refreshToken: string } // Explicit tokens
| RequestLike // Extract from request headers
| null // No storage
```
### Token Store Types
"cookie": [JS-ONLY]
@ -155,8 +173,19 @@ Many functions also accept a tokenStore parameter to override storage for that c
Use explicit token values directly.
For custom token management scenarios.
RequestLike object:
An object that conforms to whatever the requests look like in common backend frameworks. For example, in JavaScript, these often have the shape `{ headers: { get(name: string): string | null } }`, but in other languages this may drastically differ (and may not even be an interface and instead rather just be an abstract class, or not exist at all).
This exists as a simplified way to support common backend frameworks in a more accessible way than the `{ accessToken: string, refreshToken: string }` one.
Extract tokens from the x-stack-auth header:
1. Get header value: headers.get("x-stack-auth")
2. Parse as JSON: { accessToken: string, refreshToken: string }
3. Use those tokens for authentication
null:
No token storage. SDK methods requiring authentication will fail. Most useful for backends, as you can still specify the token store per-request.
No token storage. SDK methods requiring authentication will fail.
Most useful for backends, as you can still specify the token store per-request.
### x-stack-auth Header Format
@ -188,4 +217,33 @@ Methods that can return this error:
- signInWithPasskey
- callOAuthCallback
The attempt_code is short-lived and single-use.
The attempt_code is short-lived (a few minutes) and single-use.
## JWT Access Token Claims
The access token is a JWT with these claims:
| Claim | Maps to | Type |
|-------|---------|------|
| sub | id | string |
| name | displayName | string or null |
| email | primaryEmail | string or null |
| email_verified | primaryEmailVerified | boolean |
| is_anonymous | isAnonymous | boolean |
| is_restricted | isRestricted | boolean |
| restricted_reason | restrictedReason | object or null |
| exp | expiresAt | number (Unix timestamp) |
| iat | issuedAt | number (Unix timestamp) |
To decode: split by ".", base64url-decode the second segment, parse as JSON.
## Unknown Errors
If an API returns an error code not listed in the spec:
1. Create a generic StackAuthApiError with the code and message
2. Log the unknown error for debugging
3. Treat it as a general API error
This ensures forward compatibility when new error codes are added.

View File

@ -22,12 +22,24 @@ Optional:
"cookie" is JS-only due to complexity. See _utilities.spec.md for details.
urls: object
Override handler URLs. Defaults under "/handler":
Override handler URLs. Defaults:
home: "/"
signIn: "/handler/sign-in"
signUp: "/handler/sign-up"
signOut: "/handler/sign-out"
afterSignIn: "/"
afterSignUp: "/"
... see apps/backend for full list
afterSignOut: "/"
emailVerification: "/handler/email-verification"
passwordReset: "/handler/password-reset"
forgotPassword: "/handler/forgot-password"
magicLinkCallback: "/handler/magic-link-callback"
oauthCallback: "/handler/oauth-callback"
accountSettings: "/handler/account-settings"
onboarding: "/handler/onboarding"
teamInvitation: "/handler/team-invitation"
mfa: "/handler/mfa"
error: "/handler/error"
oauthScopesOnSignIn: object
Additional OAuth scopes to request during sign-in for each provider.
@ -264,10 +276,11 @@ Response:
credential_enabled: bool,
magic_link_enabled: bool,
passkey_enabled: bool,
oauth_providers: [{ id: string, type: string }],
oauth_providers: [{ id: string }],
client_team_creation_enabled: bool,
client_user_deletion_enabled: bool,
domains: [{ domain: string, handler_path: string }]
allow_user_api_keys: bool,
allow_team_api_keys: bool
}
}
@ -813,32 +826,48 @@ Does not error.
## Redirect Methods
All redirect methods take optional { replace?: bool, noRedirectBack?: bool }.
All redirect methods take optional options:
redirectToSignIn() - redirect to signIn URL
redirectToSignUp() - redirect to signUp URL
redirectToSignOut() - redirect to signOut URL
redirectToAfterSignIn() - redirect to afterSignIn URL
redirectToAfterSignUp() - redirect to afterSignUp URL
redirectToAfterSignOut() - redirect to afterSignOut URL
redirectToHome() - redirect to home URL
redirectToAccountSettings() - redirect to accountSettings URL
redirectToForgotPassword() - redirect to forgotPassword URL
redirectToPasswordReset() - redirect to passwordReset URL
redirectToEmailVerification() - redirect to emailVerification URL
redirectToOnboarding() - redirect to onboarding URL
redirectToError() - redirect to error URL
redirectToMfa() - redirect to mfa URL
redirectToTeamInvitation() - redirect to teamInvitation URL
redirectToOAuthCallback() - redirect to oauthCallback URL
redirectToMagicLinkCallback() - redirect to magicLinkCallback URL
Options:
replace: bool? - if true, replace current history entry instead of pushing
- Browser: use location.replace() instead of location.assign()
- Mobile: affects navigation stack behavior
noRedirectBack: bool? - if true, don't set after_auth_return_to param
Special behavior for signIn/signUp/onboarding:
- If URL has after_auth_return_to query param, preserve it
- Otherwise, set after_auth_return_to to current URL (for redirect after auth)
Methods:
redirectToSignIn() - redirect to signIn URL
redirectToSignUp() - redirect to signUp URL
redirectToSignOut() - redirect to signOut URL
redirectToAfterSignIn() - redirect to afterSignIn URL
redirectToAfterSignUp() - redirect to afterSignUp URL
redirectToAfterSignOut() - redirect to afterSignOut URL
redirectToHome() - redirect to home URL
redirectToAccountSettings() - redirect to accountSettings URL
redirectToForgotPassword() - redirect to forgotPassword URL
redirectToPasswordReset() - redirect to passwordReset URL
redirectToEmailVerification() - redirect to emailVerification URL
redirectToOnboarding() - redirect to onboarding URL
redirectToError() - redirect to error URL
redirectToMfa() - redirect to mfa URL
redirectToTeamInvitation() - redirect to teamInvitation URL
redirectToOAuthCallback() - redirect to oauthCallback URL
redirectToMagicLinkCallback() - redirect to magicLinkCallback URL
Special behavior for afterSignIn/afterSignUp:
- Check URL for after_auth_return_to query param and redirect there instead
Implementation:
1. Get the target URL from the urls config
2. For signIn/signUp/onboarding (unless noRedirectBack=true):
- Check if current URL has after_auth_return_to query param
- If yes: preserve it in the target URL
- If no: set after_auth_return_to to current page URL
3. For afterSignIn/afterSignUp:
- Check current URL for after_auth_return_to query param
- If present: redirect to that URL instead of the default
4. Perform redirect based on redirectMethod config:
- "browser": window.location.assign() or .replace()
- "nextjs": Next.js redirect() function [JS-ONLY]
- "none": don't redirect (for headless/API use)
- Custom navigate function: call it with the URL
All require browser or framework-specific redirect capability.
Do not error.

View File

@ -289,6 +289,12 @@ Response:
total: number
}
EmailDeliveryInfo:
delivered: number - emails successfully delivered
bounced: number - emails that bounced (hard or soft)
complained: number - emails marked as spam by recipients
total: number - total emails sent
Does not error.
@ -327,16 +333,28 @@ Arguments:
Returns: DataVaultStore
DataVaultStore has methods:
The Data Vault is a simple key-value store for storing sensitive data server-side.
Each store is isolated and identified by its ID.
DataVaultStore:
id: string - the store ID
get(key: string): Promise<string | null>
GET /api/v1/data-vault/stores/{storeId}/items/{key} [server-only]
Returns the value for the key, or null if not found.
set(key: string, value: string): Promise<void>
PUT /api/v1/data-vault/stores/{storeId}/items/{key} [server-only]
Body: { value: string }
Sets or updates the value for the key.
delete(key: string): Promise<void>
DELETE /api/v1/data-vault/stores/{storeId}/items/{key} [server-only]
Deletes the key-value pair. No error if key doesn't exist.
list(): Promise<string[]>
GET /api/v1/data-vault/stores/{storeId}/items [server-only]
Returns all keys in the store.
Does not error.

View File

@ -69,12 +69,12 @@ options: {
allowConnectedAccounts?: bool,
}
Returns: Result<void, OAuthProviderAccountIdAlreadyUsedForSignIn>
Returns: void
PATCH /api/v1/users/me/oauth-providers/{id} { allow_sign_in, allow_connected_accounts } [authenticated]
Route: apps/backend/src/app/api/latest/users/me/oauth-providers/[id]/route.ts
Errors (in Result):
Errors:
OAuthProviderAccountIdAlreadyUsedForSignIn
code: "oauth_provider_account_id_already_used_for_sign_in"
message: "This OAuth account is already linked to another user for sign-in."
@ -111,12 +111,12 @@ options: {
allowConnectedAccounts?: bool,
}
Returns: Result<void, OAuthProviderAccountIdAlreadyUsedForSignIn>
Returns: void
PATCH /api/v1/users/{userId}/oauth-providers/{id} [server-only]
Body: { account_id, email, allow_sign_in, allow_connected_accounts }
Errors (in Result):
Errors:
OAuthProviderAccountIdAlreadyUsedForSignIn
code: "oauth_provider_account_id_already_used_for_sign_in"
message: "This OAuth account is already linked to another user for sign-in."

View File

@ -0,0 +1,107 @@
# ApiKey (Base)
Base type for API keys.
## Properties
id: string
Unique API key identifier.
description: string
User-provided description of what this key is for.
expiresAt: Date | null
When the key expires, or null if it never expires.
createdAt: Date
When the key was created.
isValid: bool
Whether the key is currently valid (not expired, not revoked).
## Methods
### revoke()
DELETE /api/v1/api-keys/{id} [authenticated]
Revokes the API key immediately.
Does not error.
### update(options)
options.description: string?
options.expiresAt: Date | null?
PATCH /api/v1/api-keys/{id} { description, expires_at } [authenticated]
Does not error.
---
# UserApiKey
An API key owned by a user.
Extends: ApiKey
## Additional Properties
userId: string
The user who owns this key.
teamId: string | null
If this key is scoped to a team, the team ID.
---
# UserApiKeyFirstView
Returned only when creating a new API key. Contains the actual key value.
Extends: UserApiKey
## Additional Properties
apiKey: string
The actual API key value. Only returned once at creation time.
Store this securely - it cannot be retrieved again.
---
# TeamApiKey
An API key owned by a team.
Extends: ApiKey
## Additional Properties
teamId: string
The team that owns this key.
---
# TeamApiKeyFirstView
Returned only when creating a new team API key.
Extends: TeamApiKey
## Additional Properties
apiKey: string
The actual API key value. Only returned once at creation time.

View File

@ -0,0 +1,55 @@
# ActiveSession
Represents an active login session for a user.
## Properties
id: string
Unique session identifier.
userId: string
The user this session belongs to.
createdAt: Date
When the session was created.
isImpersonation: bool
Whether this is an impersonation session (admin viewing as user).
lastUsedAt: Date | null
When the session was last used for an API request.
isCurrentSession: bool
Whether this is the session making the current request.
geoInfo: GeoInfo | null
Geographic information about where the session was last used.
---
# GeoInfo
Geographic information derived from IP address.
## Properties
city: string | null
City name, if detected.
region: string | null
Region/state name, if detected.
country: string | null
Country code (ISO 3166-1 alpha-2), if detected.
countryName: string | null
Full country name, if detected.
latitude: number | null
Approximate latitude.
longitude: number | null
Approximate longitude.

View File

@ -0,0 +1,42 @@
# NotificationCategory
A category of notifications that users can subscribe to or unsubscribe from.
## Properties
id: string
Unique category identifier (e.g., "marketing", "product_updates", "security").
displayName: string
Human-readable name for the category.
description: string | null
Description of what notifications this category includes.
isSubscribedByDefault: bool
Whether users are subscribed to this category by default.
isUserSubscribed: bool
Whether the current user is subscribed to this category.
## Methods
### subscribe()
POST /api/v1/notification-preferences { category_id, subscribed: true } [authenticated]
Subscribes the user to this notification category.
Does not error.
### unsubscribe()
POST /api/v1/notification-preferences { category_id, subscribed: false } [authenticated]
Unsubscribes the user from this notification category.
Does not error.

View File

@ -208,6 +208,31 @@ displayName: string
prices: Price[]
---
# Price
A price point for a product.
## Properties
id: string
Unique price identifier.
amount: number
Price amount in the smallest currency unit (e.g., cents for USD).
currency: string
Three-letter currency code (e.g., "usd", "eur").
interval: "month" | "year" | null
Billing interval for subscriptions, or null for one-time purchases.
intervalCount: number | null
Number of intervals between billings (e.g., 1 for monthly, 3 for quarterly).
---
# ServerItem (server-only)

View File

@ -38,10 +38,16 @@ passkeyEnabled: bool
oauthProviders: OAuthProviderConfig[]
List of enabled OAuth providers.
Each has: id: string, type: "google" | "github" | "microsoft" | etc.
Each has: id: string
clientTeamCreationEnabled: bool
Whether clients can create teams.
clientUserDeletionEnabled: bool
Whether clients can delete their own accounts.
allowUserApiKeys: bool
Whether users can create API keys.
allowTeamApiKeys: bool
Whether teams can create API keys.

View File

@ -41,9 +41,12 @@ Returns: ServerTeamUser[]
GET /api/v1/teams/{teamId}/users [server-only]
Route: apps/backend/src/app/api/latest/teams/[teamId]/users/route.ts
ServerTeamUser extends ServerUser with:
ServerTeamUser:
Extends ServerUser with:
teamProfile: ServerTeamMemberProfile
See types/teams/team-member-profile.spec.md for ServerTeamMemberProfile.
Does not error.

View File

@ -0,0 +1,66 @@
# TeamMemberProfile
A user's profile within a specific team. Teams can have per-user display names
and profile images that differ from the user's global profile.
## Properties
displayName: string | null
The user's display name within this team.
profileImageUrl: string | null
The user's profile image URL within this team.
---
# EditableTeamMemberProfile
The current user's editable profile within a team.
Extends: TeamMemberProfile
## Methods
### update(options)
options.displayName: string | null?
options.profileImageUrl: string | null?
PATCH /api/v1/teams/{teamId}/users/me/profile { display_name, profile_image_url } [authenticated]
Updates the current user's profile within the team.
Does not error.
---
# ServerTeamMemberProfile
Server-side team member profile with additional management capabilities.
Extends: TeamMemberProfile
## Additional Properties
userId: string
The user ID this profile belongs to.
## Methods
### update(options)
options.displayName: string | null?
options.profileImageUrl: string | null?
PATCH /api/v1/teams/{teamId}/users/{userId}/profile [server-only]
Body: { display_name, profile_image_url }
Does not error.

View File

@ -66,13 +66,11 @@ Returns: TeamUser[]
GET /api/v1/teams/{teamId}/users [authenticated]
Route: apps/backend/src/app/api/latest/teams/[teamId]/users/route.ts
TeamUser has:
id: string
teamProfile: TeamMemberProfile
TeamUser:
id: string - user ID
teamProfile: TeamMemberProfile - user's profile within this team
TeamMemberProfile has:
displayName: string | null
profileImageUrl: string | null
See types/teams/team-member-profile.spec.md for TeamMemberProfile.
Does not error.
@ -83,11 +81,14 @@ Returns: TeamInvitation[]
GET /api/v1/teams/{teamId}/invitations [authenticated]
TeamInvitation has:
id: string
recipientEmail: string | null
expiresAt: Date
TeamInvitation:
id: string - invitation ID
recipientEmail: string | null - email the invitation was sent to
expiresAt: Date - when the invitation expires
revoke(): Promise<void>
DELETE /api/v1/teams/{teamId}/invitations/{id} [authenticated]
Revokes the invitation so it can no longer be accepted.
Does not error.
@ -102,8 +103,8 @@ Returns: TeamApiKeyFirstView
POST /api/v1/teams/{teamId}/api-keys { description, expires_at, scope } [authenticated]
TeamApiKeyFirstView extends TeamApiKey with:
apiKey: string - the actual key value (only shown once)
See types/common/api-keys.spec.md for TeamApiKeyFirstView.
The apiKey property is only returned once at creation time.
Does not error.
@ -114,11 +115,7 @@ Returns: TeamApiKey[]
GET /api/v1/teams/{teamId}/api-keys [authenticated]
TeamApiKey has:
id: string
description: string
expiresAt: Date | null
createdAt: Date
See types/common/api-keys.spec.md for TeamApiKey.
Does not error.

View File

@ -175,10 +175,7 @@ Returns: EditableTeamMemberProfile
GET /api/v1/teams/{teamId}/users/me/profile [authenticated]
EditableTeamMemberProfile has:
displayName: string | null
profileImageUrl: string | null
update(options): Promise<void>
See types/teams/team-member-profile.spec.md for EditableTeamMemberProfile.
Does not error.
@ -346,14 +343,7 @@ Returns: ActiveSession[]
GET /api/v1/users/me/sessions [authenticated]
ActiveSession has:
id: string
userId: string
createdAt: Date
isImpersonation: bool
lastUsedAt: Date | null
isCurrentSession: bool
geoInfo: GeoInfo?
See types/common/sessions.spec.md for ActiveSession and GeoInfo.
Does not error.
@ -402,6 +392,8 @@ Returns: UserApiKey[]
GET /api/v1/users/me/api-keys [authenticated]
See types/common/api-keys.spec.md for UserApiKey.
Does not error.
@ -416,8 +408,8 @@ Returns: UserApiKeyFirstView
POST /api/v1/users/me/api-keys { description, expires_at, scope, team_id } [authenticated]
UserApiKeyFirstView extends UserApiKey with:
apiKey: string - the actual key value (only shown once)
See types/common/api-keys.spec.md for UserApiKeyFirstView.
The apiKey property is only returned once at creation time.
Does not error.
@ -431,22 +423,69 @@ Returns: NotificationCategory[]
GET /api/v1/notification-categories [authenticated]
See types/notifications/notification-category.spec.md for NotificationCategory.
Does not error.
## Auth Methods (from StackClientApp)
## Auth Methods
signOut(options?)
Same as StackClientApp.signOut()
These methods are available on the CurrentUser object for convenience.
They operate on the user's current session.
getAccessToken()
Same as StackClientApp.getAccessToken()
getRefreshToken()
Same as StackClientApp.getRefreshToken()
### signOut(options?)
getAuthHeaders()
Same as StackClientApp.getAuthHeaders()
options.redirectUrl: string? - where to redirect after sign out
Signs out the current user by invalidating their session.
Implementation:
1. DELETE /api/v1/auth/sessions/current [authenticated]
(Ignore errors - session may already be invalid)
2. Clear stored tokens
3. Redirect to redirectUrl or afterSignOut URL
Does not error.
### getAccessToken()
Returns: string | null
Returns the current access token, refreshing if needed.
Returns null if not authenticated.
Does not error.
### getRefreshToken()
Returns: string | null
Returns the current refresh token.
Returns null if not authenticated.
Does not error.
### getAuthHeaders()
Returns: { "x-stack-auth": string }
Returns headers for cross-origin authenticated requests.
The value is JSON: { "accessToken": "<token>", "refreshToken": "<token>" }
Does not error.
### getAuthJson()
Returns: { accessToken: string | null, refreshToken: string | null }
Returns the current tokens as an object.
Does not error.
## Deprecated Methods