# Sign up anonymously Source: https://docs.hexclave.com/api/client/anonymous/sign-up-anonymously /openapi/client.json post /auth/anonymous/sign-up Create a new anonymous account with no email # Create team API key Source: https://docs.hexclave.com/api/client/api-keys/create-team-api-key /openapi/client.json post /team-api-keys Create a new API key for a user or team # Create user API key Source: https://docs.hexclave.com/api/client/api-keys/create-user-api-key /openapi/client.json post /user-api-keys Create a new API key for a user or team # Get team API key details Source: https://docs.hexclave.com/api/client/api-keys/get-team-api-key-details /openapi/client.json get /team-api-keys/{api_key_id} Get details of a specific team API key # Get user API key details Source: https://docs.hexclave.com/api/client/api-keys/get-user-api-key-details /openapi/client.json get /user-api-keys/{api_key_id} Get details of a specific user API key # List team API keys Source: https://docs.hexclave.com/api/client/api-keys/list-team-api-keys /openapi/client.json get /team-api-keys List all team API keys for the project with their metadata and status # List user API keys Source: https://docs.hexclave.com/api/client/api-keys/list-user-api-keys /openapi/client.json get /user-api-keys List all user API keys for the project with their metadata and status # Update team API key Source: https://docs.hexclave.com/api/client/api-keys/update-team-api-key /openapi/client.json patch /team-api-keys/{api_key_id} Update an team API key # Update user API key Source: https://docs.hexclave.com/api/client/api-keys/update-user-api-key /openapi/client.json patch /user-api-keys/{api_key_id} Update an user API key # /api/v1 Source: https://docs.hexclave.com/api/client/apiv1 /openapi/client.json get / Returns a human-readable message with some useful information about the API. # Complete CLI authentication Source: https://docs.hexclave.com/api/client/cli-authentication/complete-cli-authentication /openapi/client.json post /auth/cli/complete Inspect, claim, or complete a CLI authentication session # Initiate CLI authentication Source: https://docs.hexclave.com/api/client/cli-authentication/initiate-cli-authentication /openapi/client.json post /auth/cli Create a new CLI authentication session and return polling and login codes # Poll CLI authentication status Source: https://docs.hexclave.com/api/client/cli-authentication/poll-cli-authentication-status /openapi/client.json post /auth/cli/poll Check the status of a CLI authentication session using the polling code # List connected accounts Source: https://docs.hexclave.com/api/client/connected-accounts/list-connected-accounts /openapi/client.json get /connected-accounts/{user_id} Retrieves a list of all connected accounts for a user. # Check email verification code Source: https://docs.hexclave.com/api/client/contact-channels/check-email-verification-code /openapi/client.json post /contact-channels/verify/check-code Check if an email verification code is valid without using it # Create a contact channel Source: https://docs.hexclave.com/api/client/contact-channels/create-a-contact-channel /openapi/client.json post /contact-channels Add a new contact channel for a user. # Delete a contact channel Source: https://docs.hexclave.com/api/client/contact-channels/delete-a-contact-channel /openapi/client.json delete /contact-channels/{user_id}/{contact_channel_id} Removes a contact channel for a given user. # Get a contact channel Source: https://docs.hexclave.com/api/client/contact-channels/get-a-contact-channel /openapi/client.json get /contact-channels/{user_id}/{contact_channel_id} Retrieves a specific contact channel by the user ID and the contact channel ID. # List contact channels Source: https://docs.hexclave.com/api/client/contact-channels/list-contact-channels /openapi/client.json get /contact-channels Retrieves a list of all contact channels for a user. # Send contact channel verification code Source: https://docs.hexclave.com/api/client/contact-channels/send-contact-channel-verification-code /openapi/client.json post /contact-channels/{user_id}/{contact_channel_id}/send-verification-code Send a code to the user's contact channel for verifying the contact channel. # Update a contact channel Source: https://docs.hexclave.com/api/client/contact-channels/update-a-contact-channel /openapi/client.json patch /contact-channels/{user_id}/{contact_channel_id} Updates an existing contact channel. Only the values provided will be updated. # Verify an email Source: https://docs.hexclave.com/api/client/contact-channels/verify-an-email /openapi/client.json post /contact-channels/verify Verify an email address of a user # List notification preferences Source: https://docs.hexclave.com/api/client/emails/list-notification-preferences /openapi/client.json get /emails/notification-preference/{user_id} Get all notification preferences for a user, showing which notification categories are enabled or disabled. # Update notification preference Source: https://docs.hexclave.com/api/client/emails/update-notification-preference /openapi/client.json patch /emails/notification-preference/{user_id}/{notification_category_id} Enable or disable a specific notification category for a user. # Create cross-domain auth handoff redirect Source: https://docs.hexclave.com/api/client/oauth/create-cross-domain-auth-handoff-redirect /openapi/client.json post /auth/oauth/cross-domain/authorize Creates a one-time OAuth authorization code redirect for cross-domain sign-in handoff using PKCE. # Delete an OAuth provider Source: https://docs.hexclave.com/api/client/oauth/delete-an-oauth-provider /openapi/client.json delete /oauth-providers/{user_id}/{provider_id} Removes an OAuth provider for a given user. # Get an OAuth provider Source: https://docs.hexclave.com/api/client/oauth/get-an-oauth-provider /openapi/client.json get /oauth-providers/{user_id}/{provider_id} Retrieves a specific OAuth provider by the user ID and the OAuth provider ID. # List OAuth providers Source: https://docs.hexclave.com/api/client/oauth/list-oauth-providers /openapi/client.json get /oauth-providers Retrieves a list of all OAuth providers for a user. # OAuth authorize endpoint Source: https://docs.hexclave.com/api/client/oauth/oauth-authorize-endpoint /openapi/client.json get /auth/oauth/authorize/{provider_id} This endpoint is used to initiate the OAuth authorization flow. there are two purposes for this endpoint: 1. Authenticate a user with an OAuth provider. 2. Link an existing user with an OAuth provider. # OAuth token endpoints Source: https://docs.hexclave.com/api/client/oauth/oauth-token-endpoints /openapi/client.json post /auth/oauth/token This endpoint is used to exchange an authorization code or refresh token for an access token. # Update an OAuth provider Source: https://docs.hexclave.com/api/client/oauth/update-an-oauth-provider /openapi/client.json patch /oauth-providers/{user_id}/{provider_id} Updates an existing OAuth provider for a user. # Check sign in code Source: https://docs.hexclave.com/api/client/otp/check-sign-in-code /openapi/client.json post /auth/otp/sign-in/check-code Check if a sign in code is valid without using it # MFA sign in Source: https://docs.hexclave.com/api/client/otp/mfa-sign-in /openapi/client.json post /auth/mfa/sign-in Complete multi-factor authorization to sign in, with a TOTP and an MFA attempt code # Send sign-in code Source: https://docs.hexclave.com/api/client/otp/send-sign-in-code /openapi/client.json post /auth/otp/send-sign-in-code Send a code to the user's email address for sign-in. # Sign in with a code Source: https://docs.hexclave.com/api/client/otp/sign-in-with-a-code /openapi/client.json post /auth/otp/sign-in # Check reset password code Source: https://docs.hexclave.com/api/client/password/check-reset-password-code /openapi/client.json post /auth/password/reset/check-code Check if a reset password code is valid without using it # Reset password with a code Source: https://docs.hexclave.com/api/client/password/reset-password-with-a-code /openapi/client.json post /auth/password/reset Reset password with a code # Send reset password code Source: https://docs.hexclave.com/api/client/password/send-reset-password-code /openapi/client.json post /auth/password/send-reset-code Send a code to the user's email address for resetting the password. # Set password Source: https://docs.hexclave.com/api/client/password/set-password /openapi/client.json post /auth/password/set Set a new password for the current user # Sign in with email and password Source: https://docs.hexclave.com/api/client/password/sign-in-with-email-and-password /openapi/client.json post /auth/password/sign-in Sign in to an account with email and password # Sign up with email and password Source: https://docs.hexclave.com/api/client/password/sign-up-with-email-and-password /openapi/client.json post /auth/password/sign-up Create a new account with email and password # Update password Source: https://docs.hexclave.com/api/client/password/update-password /openapi/client.json post /auth/password/update Update the password of the current user, requires the old password # Create Purchase Session Source: https://docs.hexclave.com/api/client/payments/create-purchase-session /openapi/client.json post /payments/purchases/purchase-session Creates a purchase session for completing a purchase. # Create Purchase URL Source: https://docs.hexclave.com/api/client/payments/create-purchase-url /openapi/client.json post /payments/purchases/create-purchase-url Creates a secure checkout URL for purchasing a product. # Get Item Source: https://docs.hexclave.com/api/client/payments/get-item /openapi/client.json get /payments/items/{customer_type}/{customer_id}/{item_id} Retrieves information about a specific item (credits, quotas, etc.) for a customer. # Validate Purchase Code Source: https://docs.hexclave.com/api/client/payments/validate-purchase-code /openapi/client.json post /payments/purchases/validate-code Validates a purchase verification code and returns purchase details including available prices. # List project permissions Source: https://docs.hexclave.com/api/client/permissions/list-project-permissions /openapi/client.json get /project-permissions List global permissions of the current user. `user_id=me` must be set for client requests. `(user_id, permission_id)` together uniquely identify a permission. # List team permissions Source: https://docs.hexclave.com/api/client/permissions/list-team-permissions /openapi/client.json get /team-permissions List team permissions of the current user. `user_id=me` must be set for client requests. Note that this might contain the permissions with the same permission ID across different teams. `(team_id, user_id, permission_id)` together uniquely identify a permission. # Get the current project Source: https://docs.hexclave.com/api/client/projects/get-the-current-project /openapi/client.json get /projects/current Get the current project information including display name, OAuth providers and authentication methods. Useful for displaying the available login options to the user. # Delete session Source: https://docs.hexclave.com/api/client/sessions/delete-session /openapi/client.json delete /auth/sessions/{id} Delete a session by ID. # List sessions Source: https://docs.hexclave.com/api/client/sessions/list-sessions /openapi/client.json get /auth/sessions List all sessions for the current user. # Refresh access token Source: https://docs.hexclave.com/api/client/sessions/refresh-access-token /openapi/client.json post /auth/sessions/current/refresh Get a new access token using a refresh token # Sign out of the current session Source: https://docs.hexclave.com/api/client/sessions/sign-out-of-the-current-session /openapi/client.json delete /auth/sessions/current Sign out of the current session and invalidate the refresh token # Accept a team invitation by ID Source: https://docs.hexclave.com/api/client/teams/accept-a-team-invitation-by-id /openapi/client.json post /team-invitations/{id}/accept Accepts a team invitation for the specified user. The user must have a verified email matching the invitation's recipient email. This marks the invitation as used and adds the user to the team. # Accept the team invitation Source: https://docs.hexclave.com/api/client/teams/accept-the-team-invitation /openapi/client.json post /team-invitations/accept Accept invitation and add user to the team # Check if a team invitation code is valid Source: https://docs.hexclave.com/api/client/teams/check-if-a-team-invitation-code-is-valid /openapi/client.json post /team-invitations/accept/check-code Check if a team invitation code is valid without using it # Create a team Source: https://docs.hexclave.com/api/client/teams/create-a-team /openapi/client.json post /teams Create a new team and optionally add the current user as a member. # Delete a team Source: https://docs.hexclave.com/api/client/teams/delete-a-team /openapi/client.json delete /teams/{team_id} Delete a team. Only allowed if the current user is a member of the team and has the `$delete_team` permission. # Delete a team invitation Source: https://docs.hexclave.com/api/client/teams/delete-a-team-invitation /openapi/client.json delete /team-invitations/{id} # Get a team Source: https://docs.hexclave.com/api/client/teams/get-a-team /openapi/client.json get /teams/{team_id} Get a team that the current user is a member of. # Get a team member profile Source: https://docs.hexclave.com/api/client/teams/get-a-team-member-profile /openapi/client.json get /team-member-profiles/{team_id}/{user_id} Get a team member profile. you can always get your own profile by setting `me` as the `user_id` in the path parameters on the client. If you want to get someone else's profile in a team, you need to have the `$read_members` permission in that team. # Get team invitation details Source: https://docs.hexclave.com/api/client/teams/get-team-invitation-details /openapi/client.json post /team-invitations/accept/details Get additional information about a team invitation code # List team invitations Source: https://docs.hexclave.com/api/client/teams/list-team-invitations /openapi/client.json get /team-invitations # List team members profiles Source: https://docs.hexclave.com/api/client/teams/list-team-members-profiles /openapi/client.json get /team-member-profiles List team members profiles. You always need to specify a `team_id` that your are a member of on the client. You can always filter for your own profile by setting `me` as the `user_id` in the path parameters. If you want list all the profiles in a team, you need to have the `$read_members` permission in that team. # List teams Source: https://docs.hexclave.com/api/client/teams/list-teams /openapi/client.json get /teams List all the teams that the current user is a member of. `user_id=me` must be passed in the query parameters. # Remove a user from a team Source: https://docs.hexclave.com/api/client/teams/remove-a-user-from-a-team /openapi/client.json delete /team-memberships/{team_id}/{user_id} All the users are allowed to remove themselves from a team (`user_id=me`). Only the users who have the `$remove_members` permission are allowed to remove other users from a team. `team_id` is must an ID of a team that the user is a member of. # Send an email to invite a user to a team Source: https://docs.hexclave.com/api/client/teams/send-an-email-to-invite-a-user-to-a-team /openapi/client.json post /team-invitations/send-code The user receiving this email can join the team by clicking on the link in the email. If the user does not have an account yet, they will be prompted to create one. # Update a team Source: https://docs.hexclave.com/api/client/teams/update-a-team /openapi/client.json patch /teams/{team_id} Update the team information. Only allowed if the current user is a member of the team and has the `$update_team` permission. # Update your team member profile Source: https://docs.hexclave.com/api/client/teams/update-your-team-member-profile /openapi/client.json patch /team-member-profiles/{team_id}/{user_id} Update your own team member profile. `user_id` must be `me` in the path parameters on the client. # Delete current user Source: https://docs.hexclave.com/api/client/users/delete-current-user /openapi/client.json delete /users/me Deletes the currently authenticated user. Use this with caution. # Get current user Source: https://docs.hexclave.com/api/client/users/get-current-user /openapi/client.json get /users/me Gets the currently authenticated user. # Update current user Source: https://docs.hexclave.com/api/client/users/update-current-user /openapi/client.json patch /users/me Updates the currently authenticated user. Only the values provided will be updated. # Overview Source: https://docs.hexclave.com/api/overview Complete REST API documentation for Hexclave Stack offers a REST API for backends & frontends of any programming language or framework. This API is used to authenticate users, manage user data, and more. ## Authentication Hexclave uses different authentication patterns depending on whether you're making requests from client-side code (browser, mobile app) or server-side code (your backend). **Security Critical**: Never expose your secret server key (`ssk_...`) in client-side code, browser requests, or any publicly accessible location. Server keys should only be used in secure backend environments. ### Client-Side Authentication For requests from browsers, mobile apps, or other client-side environments: ```bash theme={null} curl https://api.hexclave.com/api/v1/ \ -H "X-Stack-Access-Type: client" \ -H "X-Stack-Project-Id: " \ -H "X-Stack-Publishable-Client-Key: pck_" \ -H "X-Stack-Access-Token: " ``` ### Server-Side Authentication For requests from your secure backend server: ```bash theme={null} curl https://api.hexclave.com/api/v1/ \ -H "X-Stack-Access-Type: server" \ -H "X-Stack-Project-Id: " \ -H "X-Stack-Secret-Server-Key: ssk_" ``` ### Authentication Headers | Header | Type | Used In | Description | | -------------------------------- | ---------------------- | ----------- | ---------------------------------------------------------------------------------------- | | `X-Stack-Access-Type` | `"client" \| "server"` | Both | Required. Use `"client"` for frontend/browser requests, `"server"` for backend requests. | | `X-Stack-Project-Id` | UUID | Both | Required. Your project ID from the Stack dashboard. | | `X-Stack-Publishable-Client-Key` | string | Client only | Required for client access. Safe to expose in frontend code. Starts with `pck_`. | | `X-Stack-Secret-Server-Key` | string | Server only | Required for server access. **Never expose in client code**. Starts with `ssk_`. | | `X-Stack-Access-Token` | string | Client only | Optional. The current user's access token. Used to act on behalf of a specific user. | To set up a backend in JavaScript, Python, or another language using the REST API, see [Setup](/guides/getting-started/setup). ## Getting Started Select the API category that matches your use case. Configure the appropriate authentication method (sessions, API keys, or webhook verification). Use the documented endpoints with proper authentication headers. Process the API responses according to the documentation and error handling guidelines. ## FAQ Any language that has the ability to send HTTP requests can use the Stack REST API. This includes JavaScript, Python, Ruby, Java, Go, C#, Dart, and many more. **Client access type** (`X-Stack-Access-Type: client`) is for client-side applications like browsers and mobile apps. Client APIs can only read and update the currently authenticated user's data. Use your publishable client key (`pck_...`) - it's safe to include in frontend code. **Server access type** (`X-Stack-Access-Type: server`) is for your secure backend server. It has full access over all user data using your secret server key (`ssk_...`). Never use server access type or secret server keys in client-side code, browser requests, or any publicly accessible location. Always keep server keys secure on your backend. For more information, see the [`HexclaveClientApp` and `HexclaveServerApp` SDK reference](/sdk/objects/hexclave-app). If you'd like to build your own version of the Stack dashboard (or update project configuration programmatically), you can use the `admin` access type. These endpoints are very dangerous and you should only use them if you know what you're doing. For more information, see the [`HexclaveClientApp` and `HexclaveServerApp` SDK reference](/sdk/objects/hexclave-app). Hexclave API returns standard HTTP status codes. Common error responses include: * `400 Bad Request` - Invalid request parameters * `401 Unauthorized` - Invalid or missing authentication * `403 Forbidden` - Insufficient permissions * `404 Not Found` - Resource not found * `429 Too Many Requests` - Rate limit exceeded * `500 Internal Server Error` - Server error Error responses include a JSON body with additional details about the error. Yes, Hexclave implements rate limiting to ensure fair usage and system stability. Rate limits vary by endpoint and access type. When you exceed the rate limit, you'll receive a `429 Too Many Requests` response with headers indicating when you can retry. ## Need Help? Check the Getting Started Guide for initial setup. Visit the Concepts section for Hexclave fundamentals. Join the Discord community for support and discussions. # Get analytics query timing Source: https://docs.hexclave.com/api/server/analytics/get-analytics-query-timing /openapi/server.json post /analytics/query/timing Returns CPU and wall-clock timing stats for a previously run analytics query. # Run analytics query Source: https://docs.hexclave.com/api/server/analytics/run-analytics-query /openapi/server.json post /analytics/query Runs a read-only ClickHouse SQL query against the current project's analytics dataset. # Sign up anonymously Source: https://docs.hexclave.com/api/server/anonymous/sign-up-anonymously /openapi/server.json post /auth/anonymous/sign-up Create a new anonymous account with no email # Check team API key validity Source: https://docs.hexclave.com/api/server/api-keys/check-team-api-key-validity /openapi/server.json post /team-api-keys/check Validate a team API key # Check user API key validity Source: https://docs.hexclave.com/api/server/api-keys/check-user-api-key-validity /openapi/server.json post /user-api-keys/check Validate a user API key # Create team API key Source: https://docs.hexclave.com/api/server/api-keys/create-team-api-key /openapi/server.json post /team-api-keys Create a new API key for a user or team # Create user API key Source: https://docs.hexclave.com/api/server/api-keys/create-user-api-key /openapi/server.json post /user-api-keys Create a new API key for a user or team # Get team API key details Source: https://docs.hexclave.com/api/server/api-keys/get-team-api-key-details /openapi/server.json get /team-api-keys/{api_key_id} Get details of a specific team API key # Get user API key details Source: https://docs.hexclave.com/api/server/api-keys/get-user-api-key-details /openapi/server.json get /user-api-keys/{api_key_id} Get details of a specific user API key # List team API keys Source: https://docs.hexclave.com/api/server/api-keys/list-team-api-keys /openapi/server.json get /team-api-keys List all team API keys for the project with their metadata and status # List user API keys Source: https://docs.hexclave.com/api/server/api-keys/list-user-api-keys /openapi/server.json get /user-api-keys List all user API keys for the project with their metadata and status # Update team API key Source: https://docs.hexclave.com/api/server/api-keys/update-team-api-key /openapi/server.json patch /team-api-keys/{api_key_id} Update an team API key # Update user API key Source: https://docs.hexclave.com/api/server/api-keys/update-user-api-key /openapi/server.json patch /user-api-keys/{api_key_id} Update an user API key # /api/v1 Source: https://docs.hexclave.com/api/server/apiv1 /openapi/server.json get / Returns a human-readable message with some useful information about the API. # Complete CLI authentication Source: https://docs.hexclave.com/api/server/cli-authentication/complete-cli-authentication /openapi/server.json post /auth/cli/complete Inspect, claim, or complete a CLI authentication session # Initiate CLI authentication Source: https://docs.hexclave.com/api/server/cli-authentication/initiate-cli-authentication /openapi/server.json post /auth/cli Create a new CLI authentication session and return polling and login codes # Poll CLI authentication status Source: https://docs.hexclave.com/api/server/cli-authentication/poll-cli-authentication-status /openapi/server.json post /auth/cli/poll Check the status of a CLI authentication session using the polling code # List connected accounts Source: https://docs.hexclave.com/api/server/connected-accounts/list-connected-accounts /openapi/server.json get /connected-accounts/{user_id} Retrieves a list of all connected accounts for a user. # Check email verification code Source: https://docs.hexclave.com/api/server/contact-channels/check-email-verification-code /openapi/server.json post /contact-channels/verify/check-code Check if an email verification code is valid without using it # Create a contact channel Source: https://docs.hexclave.com/api/server/contact-channels/create-a-contact-channel /openapi/server.json post /contact-channels Add a new contact channel for a user. # Delete a contact channel Source: https://docs.hexclave.com/api/server/contact-channels/delete-a-contact-channel /openapi/server.json delete /contact-channels/{user_id}/{contact_channel_id} Removes a contact channel for a given user. # Get a contact channel Source: https://docs.hexclave.com/api/server/contact-channels/get-a-contact-channel /openapi/server.json get /contact-channels/{user_id}/{contact_channel_id} Retrieves a specific contact channel by the user ID and the contact channel ID. # List contact channels Source: https://docs.hexclave.com/api/server/contact-channels/list-contact-channels /openapi/server.json get /contact-channels Retrieves a list of all contact channels for a user. # Send contact channel verification code Source: https://docs.hexclave.com/api/server/contact-channels/send-contact-channel-verification-code /openapi/server.json post /contact-channels/{user_id}/{contact_channel_id}/send-verification-code Send a code to the user's contact channel for verifying the contact channel. # Update a contact channel Source: https://docs.hexclave.com/api/server/contact-channels/update-a-contact-channel /openapi/server.json patch /contact-channels/{user_id}/{contact_channel_id} Updates an existing contact channel. Only the values provided will be updated. # Verify an email Source: https://docs.hexclave.com/api/server/contact-channels/verify-an-email /openapi/server.json post /contact-channels/verify Verify an email address of a user # Retrieve encrypted value from data vault Source: https://docs.hexclave.com/api/server/datavault/retrieve-encrypted-value-from-data-vault /openapi/server.json post /data-vault/stores/{id}/get Retrieves and decrypts a value from the data vault using a hashed key # Store encrypted value in data vault Source: https://docs.hexclave.com/api/server/datavault/store-encrypted-value-in-data-vault /openapi/server.json post /data-vault/stores/{id}/set Stores a hashed key and encrypted value in the data vault for a specific store # Activate email capacity boost Source: https://docs.hexclave.com/api/server/emails/activate-email-capacity-boost /openapi/server.json post /emails/capacity-boost Temporarily increases email capacity by 4x for 4 hours. # Get email delivery info Source: https://docs.hexclave.com/api/server/emails/get-email-delivery-info /openapi/server.json get /emails/delivery-info Returns delivery statistics and capacity information for the current tenancy. # Get email outbox entry Source: https://docs.hexclave.com/api/server/emails/get-email-outbox-entry /openapi/server.json get /emails/outbox/{id} Gets a single email from the outbox by ID. # List email outbox Source: https://docs.hexclave.com/api/server/emails/list-email-outbox /openapi/server.json get /emails/outbox Lists all emails in the outbox with optional filtering by status, simple_status, or user_id. # List notification preferences Source: https://docs.hexclave.com/api/server/emails/list-notification-preferences /openapi/server.json get /emails/notification-preference/{user_id} Get all notification preferences for a user, showing which notification categories are enabled or disabled. # Send email Source: https://docs.hexclave.com/api/server/emails/send-email /openapi/server.json post /emails/send-email Send an email to a list of users (user_ids), all users (all_users), or arbitrary email addresses (emails). The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email. # Update email outbox entry Source: https://docs.hexclave.com/api/server/emails/update-email-outbox-entry /openapi/server.json patch /emails/outbox/{id} Updates an email in the outbox. Can be used to edit email content, pause/resume, or cancel emails. Only emails in editable states (`paused`, `preparing`, `rendering`, `render-error`, `scheduled`, `queued`, `server-error`) can be modified. # Update notification preference Source: https://docs.hexclave.com/api/server/emails/update-notification-preference /openapi/server.json patch /emails/notification-preference/{user_id}/{notification_category_id} Enable or disable a specific notification category for a user. # Create an OAuth provider Source: https://docs.hexclave.com/api/server/oauth/create-an-oauth-provider /openapi/server.json post /oauth-providers Add a new OAuth provider for a user. # Create cross-domain auth handoff redirect Source: https://docs.hexclave.com/api/server/oauth/create-cross-domain-auth-handoff-redirect /openapi/server.json post /auth/oauth/cross-domain/authorize Creates a one-time OAuth authorization code redirect for cross-domain sign-in handoff using PKCE. # Delete an OAuth provider Source: https://docs.hexclave.com/api/server/oauth/delete-an-oauth-provider /openapi/server.json delete /oauth-providers/{user_id}/{provider_id} Removes an OAuth provider for a given user. # Get an OAuth provider Source: https://docs.hexclave.com/api/server/oauth/get-an-oauth-provider /openapi/server.json get /oauth-providers/{user_id}/{provider_id} Retrieves a specific OAuth provider by the user ID and the OAuth provider ID. # List OAuth providers Source: https://docs.hexclave.com/api/server/oauth/list-oauth-providers /openapi/server.json get /oauth-providers Retrieves a list of all OAuth providers for a user. # OAuth authorize endpoint Source: https://docs.hexclave.com/api/server/oauth/oauth-authorize-endpoint /openapi/server.json get /auth/oauth/authorize/{provider_id} This endpoint is used to initiate the OAuth authorization flow. there are two purposes for this endpoint: 1. Authenticate a user with an OAuth provider. 2. Link an existing user with an OAuth provider. # OAuth token endpoints Source: https://docs.hexclave.com/api/server/oauth/oauth-token-endpoints /openapi/server.json post /auth/oauth/token This endpoint is used to exchange an authorization code or refresh token for an access token. # Update an OAuth provider Source: https://docs.hexclave.com/api/server/oauth/update-an-oauth-provider /openapi/server.json patch /oauth-providers/{user_id}/{provider_id} Updates an existing OAuth provider. Only the values provided will be updated. # Create a browser action Source: https://docs.hexclave.com/api/server/others/create-a-browser-action /openapi/server.json post /browser-actions Creates a single-use link to one of the project's trusted origins that, when opened, makes the Hexclave SDK on that page perform an action in the browser: signing in as a given user (impersonation) or showing the clickmap overlay. Requires server or higher access. # Check sign in code Source: https://docs.hexclave.com/api/server/otp/check-sign-in-code /openapi/server.json post /auth/otp/sign-in/check-code Check if a sign in code is valid without using it # MFA sign in Source: https://docs.hexclave.com/api/server/otp/mfa-sign-in /openapi/server.json post /auth/mfa/sign-in Complete multi-factor authorization to sign in, with a TOTP and an MFA attempt code # Send sign-in code Source: https://docs.hexclave.com/api/server/otp/send-sign-in-code /openapi/server.json post /auth/otp/send-sign-in-code Send a code to the user's email address for sign-in. # Sign in with a code Source: https://docs.hexclave.com/api/server/otp/sign-in-with-a-code /openapi/server.json post /auth/otp/sign-in # Check reset password code Source: https://docs.hexclave.com/api/server/password/check-reset-password-code /openapi/server.json post /auth/password/reset/check-code Check if a reset password code is valid without using it # Reset password with a code Source: https://docs.hexclave.com/api/server/password/reset-password-with-a-code /openapi/server.json post /auth/password/reset Reset password with a code # Send reset password code Source: https://docs.hexclave.com/api/server/password/send-reset-password-code /openapi/server.json post /auth/password/send-reset-code Send a code to the user's email address for resetting the password. # Set password Source: https://docs.hexclave.com/api/server/password/set-password /openapi/server.json post /auth/password/set Set a new password for the current user # Sign in with email and password Source: https://docs.hexclave.com/api/server/password/sign-in-with-email-and-password /openapi/server.json post /auth/password/sign-in Sign in to an account with email and password # Sign up with email and password Source: https://docs.hexclave.com/api/server/password/sign-up-with-email-and-password /openapi/server.json post /auth/password/sign-up Create a new account with email and password # Update password Source: https://docs.hexclave.com/api/server/password/update-password /openapi/server.json post /auth/password/update Update the password of the current user, requires the old password # Create Purchase Session Source: https://docs.hexclave.com/api/server/payments/create-purchase-session /openapi/server.json post /payments/purchases/purchase-session Creates a purchase session for completing a purchase. # Create Purchase URL Source: https://docs.hexclave.com/api/server/payments/create-purchase-url /openapi/server.json post /payments/purchases/create-purchase-url Creates a secure checkout URL for purchasing a product. # Get Item Source: https://docs.hexclave.com/api/server/payments/get-item /openapi/server.json get /payments/items/{customer_type}/{customer_id}/{item_id} Retrieves information about a specific item (credits, quotas, etc.) for a customer. # Update Item Quantity Source: https://docs.hexclave.com/api/server/payments/update-item-quantity /openapi/server.json post /payments/items/{customer_type}/{customer_id}/{item_id}/update-quantity Updates the quantity of an item for a customer. Can increase or decrease quantities, with optional expiration and negative balance control. # Validate Purchase Code Source: https://docs.hexclave.com/api/server/payments/validate-purchase-code /openapi/server.json post /payments/purchases/validate-code Validates a purchase verification code and returns purchase details including available prices. # Grant a global permission to a user Source: https://docs.hexclave.com/api/server/permissions/grant-a-global-permission-to-a-user /openapi/server.json post /project-permissions/{user_id}/{permission_id} Grant a global permission to a user (the permission must be created first on the Hexclave dashboard) # Grant a team permission to a user Source: https://docs.hexclave.com/api/server/permissions/grant-a-team-permission-to-a-user /openapi/server.json post /team-permissions/{team_id}/{user_id}/{permission_id} Grant a team permission to a user (the team permission must be created first on the Hexclave dashboard) # List project permissions Source: https://docs.hexclave.com/api/server/permissions/list-project-permissions /openapi/server.json get /project-permissions Query and filter the permission with `user_id` and `permission_id`. `(user_id, permission_id)` together uniquely identify a permission. # List team permissions of a user Source: https://docs.hexclave.com/api/server/permissions/list-team-permissions-of-a-user /openapi/server.json get /team-permissions Query and filter the permission with `team_id`, `user_id`, and `permission_id`. Note that this might contain the permissions with the same permission ID across different teams and users. `(team_id, user_id, permission_id)` together uniquely identify a permission. # Revoke a global permission from a user Source: https://docs.hexclave.com/api/server/permissions/revoke-a-global-permission-from-a-user /openapi/server.json delete /project-permissions/{user_id}/{permission_id} Revoke a global permission from a user # Revoke a team permission from a user Source: https://docs.hexclave.com/api/server/permissions/revoke-a-team-permission-from-a-user /openapi/server.json delete /team-permissions/{team_id}/{user_id}/{permission_id} Revoke a team permission from a user # Get the current project Source: https://docs.hexclave.com/api/server/projects/get-the-current-project /openapi/server.json get /projects/current Get the current project information including display name, OAuth providers and authentication methods. Useful for displaying the available login options to the user. # Get session replay Source: https://docs.hexclave.com/api/server/session-replays/get-session-replay /openapi/server.json get /session-replays/{session_replay_id} Returns metadata for a single session replay, including the user it belongs to, its start and last-event timestamps, and its chunk and event counts. # Get session replay chunk events Source: https://docs.hexclave.com/api/server/session-replays/get-session-replay-chunk-events /openapi/server.json get /session-replays/{session_replay_id}/chunks/{chunk_id}/events Returns the raw rrweb events for a single chunk of a session replay. These are the events a replay player consumes. # Get session replay events Source: https://docs.hexclave.com/api/server/session-replays/get-session-replay-events /openapi/server.json get /session-replays/{session_replay_id}/events Returns chunks of a session replay together with their raw rrweb events, in a single request. Use the offset and limit parameters to page through a long recording. # List session replay chunks Source: https://docs.hexclave.com/api/server/session-replays/list-session-replay-chunks /openapi/server.json get /session-replays/{session_replay_id}/chunks Lists the chunks of a session replay in recording order, with cursor-based pagination. Each chunk describes a batch of recorded events without including the events themselves. # List session replays Source: https://docs.hexclave.com/api/server/session-replays/list-session-replays /openapi/server.json get /session-replays Lists session replays for the project, most recently active first. Supports filtering by user, team, duration, last-event time, and click count, and cursor-based pagination. # Create session Source: https://docs.hexclave.com/api/server/sessions/create-session /openapi/server.json post /auth/sessions Create a new session for a given user. This will return a refresh token that can be used to impersonate the user. # Delete session Source: https://docs.hexclave.com/api/server/sessions/delete-session /openapi/server.json delete /auth/sessions/{id} Delete a session by ID. # List sessions Source: https://docs.hexclave.com/api/server/sessions/list-sessions /openapi/server.json get /auth/sessions List all sessions for the current user. # Refresh access token Source: https://docs.hexclave.com/api/server/sessions/refresh-access-token /openapi/server.json post /auth/sessions/current/refresh Get a new access token using a refresh token # Sign out of the current session Source: https://docs.hexclave.com/api/server/sessions/sign-out-of-the-current-session /openapi/server.json delete /auth/sessions/current Sign out of the current session and invalidate the refresh token # Accept a team invitation by ID Source: https://docs.hexclave.com/api/server/teams/accept-a-team-invitation-by-id /openapi/server.json post /team-invitations/{id}/accept Accepts a team invitation for the specified user. The user must have a verified email matching the invitation's recipient email. This marks the invitation as used and adds the user to the team. # Accept the team invitation Source: https://docs.hexclave.com/api/server/teams/accept-the-team-invitation /openapi/server.json post /team-invitations/accept Accept invitation and add user to the team # Add a user to a team Source: https://docs.hexclave.com/api/server/teams/add-a-user-to-a-team /openapi/server.json post /team-memberships/{team_id}/{user_id} # Check if a team invitation code is valid Source: https://docs.hexclave.com/api/server/teams/check-if-a-team-invitation-code-is-valid /openapi/server.json post /team-invitations/accept/check-code Check if a team invitation code is valid without using it # Create a team Source: https://docs.hexclave.com/api/server/teams/create-a-team /openapi/server.json post /teams Create a new team and optionally add the current user as a member. # Delete a team Source: https://docs.hexclave.com/api/server/teams/delete-a-team /openapi/server.json delete /teams/{team_id} Delete a team by ID. # Delete a team invitation Source: https://docs.hexclave.com/api/server/teams/delete-a-team-invitation /openapi/server.json delete /team-invitations/{id} # Get a team Source: https://docs.hexclave.com/api/server/teams/get-a-team /openapi/server.json get /teams/{team_id} Get a team by ID. # Get a team member profile Source: https://docs.hexclave.com/api/server/teams/get-a-team-member-profile /openapi/server.json get /team-member-profiles/{team_id}/{user_id} Get a team member profile by user ID # Get team invitation details Source: https://docs.hexclave.com/api/server/teams/get-team-invitation-details /openapi/server.json post /team-invitations/accept/details Get additional information about a team invitation code # List team invitations Source: https://docs.hexclave.com/api/server/teams/list-team-invitations /openapi/server.json get /team-invitations # List team members profiles Source: https://docs.hexclave.com/api/server/teams/list-team-members-profiles /openapi/server.json get /team-member-profiles List team members profiles and filter by team ID and user ID # List teams Source: https://docs.hexclave.com/api/server/teams/list-teams /openapi/server.json get /teams List all the teams in the project. # Remove a user from a team Source: https://docs.hexclave.com/api/server/teams/remove-a-user-from-a-team /openapi/server.json delete /team-memberships/{team_id}/{user_id} # Send an email to invite a user to a team Source: https://docs.hexclave.com/api/server/teams/send-an-email-to-invite-a-user-to-a-team /openapi/server.json post /team-invitations/send-code The user receiving this email can join the team by clicking on the link in the email. If the user does not have an account yet, they will be prompted to create one. # Update a team Source: https://docs.hexclave.com/api/server/teams/update-a-team /openapi/server.json patch /teams/{team_id} Update the team information by ID. # Update a team member profile Source: https://docs.hexclave.com/api/server/teams/update-a-team-member-profile /openapi/server.json patch /team-member-profiles/{team_id}/{user_id} Update a team member profile by user ID # Create user Source: https://docs.hexclave.com/api/server/users/create-user /openapi/server.json post /users Creates a new user. E-mail authentication is always enabled, and no password is set, meaning the only way to authenticate the newly created user is through magic link. # Delete current user Source: https://docs.hexclave.com/api/server/users/delete-current-user /openapi/server.json delete /users/me Deletes the currently authenticated user. Use this with caution. # Delete user Source: https://docs.hexclave.com/api/server/users/delete-user /openapi/server.json delete /users/{user_id} Deletes a user. Use this with caution. # Get current user Source: https://docs.hexclave.com/api/server/users/get-current-user /openapi/server.json get /users/me Gets the currently authenticated user. # Get user Source: https://docs.hexclave.com/api/server/users/get-user /openapi/server.json get /users/{user_id} Gets a user by user ID. # List users Source: https://docs.hexclave.com/api/server/users/list-users /openapi/server.json get /users Lists all the users in the project. By default, only fully onboarded users are returned. Restricted users (those who haven't completed onboarding requirements like email verification) are included if `include_restricted` is set to `true`. Anonymous users are included if `include_anonymous` is set to `true` (which also includes restricted users). # Update current user Source: https://docs.hexclave.com/api/server/users/update-current-user /openapi/server.json patch /users/me Updates the currently authenticated user. Only the values provided will be updated. # Update user Source: https://docs.hexclave.com/api/server/users/update-user /openapi/server.json patch /users/{user_id} Updates a user. Only the values provided will be updated. # Team membershipcreated Source: https://docs.hexclave.com/api/webhooks/teams/team_membershipcreated /openapi/webhooks.json webhook team_membership.created This event is triggered when a user is added to a team. # Team membershipdeleted Source: https://docs.hexclave.com/api/webhooks/teams/team_membershipdeleted /openapi/webhooks.json webhook team_membership.deleted This event is triggered when a user is removed from a team. # Team permissioncreated Source: https://docs.hexclave.com/api/webhooks/teams/team_permissioncreated /openapi/webhooks.json webhook team_permission.created This event is triggered when a team permission is created. # Team permissiondeleted Source: https://docs.hexclave.com/api/webhooks/teams/team_permissiondeleted /openapi/webhooks.json webhook team_permission.deleted This event is triggered when a team permission is deleted. # Teamcreated Source: https://docs.hexclave.com/api/webhooks/teams/teamcreated /openapi/webhooks.json webhook team.created This event is triggered when a team is created. # Teamdeleted Source: https://docs.hexclave.com/api/webhooks/teams/teamdeleted /openapi/webhooks.json webhook team.deleted This event is triggered when a team is deleted. # Teamupdated Source: https://docs.hexclave.com/api/webhooks/teams/teamupdated /openapi/webhooks.json webhook team.updated This event is triggered when a team is updated. # Usercreated Source: https://docs.hexclave.com/api/webhooks/users/usercreated /openapi/webhooks.json webhook user.created This event is triggered when a user is created. # Userdeleted Source: https://docs.hexclave.com/api/webhooks/users/userdeleted /openapi/webhooks.json webhook user.deleted This event is triggered when a user is deleted. # Userupdated Source: https://docs.hexclave.com/api/webhooks/users/userupdated /openapi/webhooks.json webhook user.updated This event is triggered when a user is updated. # Analytics Source: https://docs.hexclave.com/guides/apps/analytics/overview Explore events, session replays, and SQL queries in your project's analytics dataset The Analytics app gives you direct access to your project's analytics dataset in Hexclave. You can inspect raw event tables, run ClickHouse SQL queries, and watch session replays to debug real user behavior. ## Overview Analytics is organized into four areas in the dashboard: * **Tables**: Browse rows across tables (events, users, contact channels, and more) with sorting, search, and incremental loading * **Queries**: Run and save reusable ClickHouse SQL queries * **Replays**: Watch session replays and filter by user, team, duration, activity window, and click count * **Clickmaps**: See which elements get clicks on your pages See [Replays & Clickmaps](./replays-and-clickmaps) for the full guide to session replays and clickmaps. ## How Analytics Works Hexclave records analytics events and replay chunks, then exposes them through the Analytics app for read-only querying and investigation. User activity in your app flows into Hexclave event ingestion, which stores data in ClickHouse. This data powers the Tables view, the SQL query runner, and the Session replay UI. Analytics isn't only client events. Hexclave also exposes product data from the rest of your project — users, contact channels, teams, permissions, email outbox, and more — in the same read-only ClickHouse dataset, so you can join product state with behavior in one place. See [Queries & Tables](./queries-and-tables) for the full table list. ### What Gets Tracked Hexclave collects both client-side and server-side analytics events: * **Client-side events**: browser interaction events like `$page-view` and `$click` * **Server-side events**: currently `$token-refresh` and `$sign-up-rule-trigger` ## Enabling the Analytics App To use analytics in your project: 1. Open your Hexclave dashboard 2. Go to **Apps** 3. Open **Analytics** 4. Click **Enable** ## Quick Start 1. Enable Analytics in your Hexclave dashboard (**Apps -> Analytics**) 2. Initialize Hexclave on your frontend with `HexclaveClientApp`/`HexclaveProvider` and a persistent `tokenStore` (e.g. `"cookie"`, or `"nextjs-cookie"` in Next.js) 3. Sign in with a real user session 4. Open the app and navigate/click around 5. Check **Analytics -> Tables** to confirm events are arriving SDK analytics capture and session replay recording are **on by default** once the Analytics app is enabled — you do not need to set `analytics.enabled` or `analytics.replays.enabled` unless you want to opt out (pass `analytics: { enabled: false }` or `analytics: { replays: { enabled: false } }`). After setup, Hexclave automatically captures client-side `$page-view` and `$click` events. See [Replays & Clickmaps](./replays-and-clickmaps) to tune replay privacy or opt out. ## Tables The **Tables** screen is the fastest way to inspect recent analytics records. * Opens on `events` by default; pick other tables from the sidebar * Built-in ordering and client-side search * Relative/absolute timestamp display toggle * Row detail dialog for inspecting full JSON payloads Use this view when you need to quickly answer "what just happened?" without writing SQL. ## Queries The **Queries** screen is a ClickHouse SQL workspace for deeper analysis. * Run read-only SQL queries with a timeout budget * Query the users and analytics tables * Save reusable queries into folders * Re-run saved queries with one click * Edit and overwrite saved query definitions ## Session Replays The **Replays** screen helps you move from "an event happened" to "what the user actually saw." * Filter sessions by user, team, duration, recency, and click count * Play back multi-tab sessions * Control playback speed * Optionally skip inactive ranges * Jump across click/page-view timeline markers Use replays when metrics alone are not enough to explain user behavior. ### Replay recording in the SDK Session replay recording is **enabled by default** once Analytics is on and your client app uses a persistent token store. You can tune privacy or opt out via the `analytics.replays` options: ```ts theme={null} import { HexclaveClientApp } from "@hexclave/js"; export const hexclaveClientApp = new HexclaveClientApp({ // ...your existing client app options tokenStore: "cookie", // use "nextjs-cookie" in Next.js analytics: { replays: { // Optional. Defaults to true when the Analytics app is enabled; set to false to opt out. enabled: true, // Optional. Defaults to true. maskAllInputs: true, }, }, }); ``` `maskAllInputs` defaults to `true`, so form fields are masked unless you explicitly disable it. For the full set of privacy controls, playback, and clickmaps, see [Replays & Clickmaps](./replays-and-clickmaps). ### Disabling Analytics Capture in the SDK SDK-managed analytics capture is enabled by default. You can disable it by disabling the Analytics app in the config or dashboard. If you don't want the SDK to collect any analytics data at all but would like to keep the Analytics app enabled, you can also pass `analytics: { enabled: false }` when creating your client app: ```ts theme={null} import { HexclaveClientApp } from "@hexclave/js"; export const hexclaveClientApp = new HexclaveClientApp({ // ...your existing client app options tokenStore: "cookie", // use "nextjs-cookie" in Next.js analytics: { enabled: false }, }); ``` This stops the SDK from sending `$page-view` and `$click` events. If you'd rather keep analytics, enable the Analytics app in your dashboard (**Apps -> Analytics**) instead. ## Best Practices 1. **Use Tables for quick incident triage**: the Tables UI is the fastest way to inspect recent rows (events, users, and more) without writing SQL. 2. **Use Queries for repeatable analysis**: save important SQL in folders, and scope queries with filters/`LIMIT` so they stay within result and timeout limits. 3. **Use Replays for behavioral debugging**: start from an event pattern, then inspect matching session replays to understand what users actually did. 4. **Use Clickmaps for UI friction and attention**: overlay click counts on your live pages to see whether a flow takes unnecessary clicks, whether a control looks interactable, or how variants compare in an A/B test. Clicks are tied to DOM elements (not pixel coordinates), so a marker in the middle of a button does not mean the user clicked the middle of that button. See [Replays & Clickmaps](./replays-and-clickmaps). 5. **Keep replay privacy defaults on**: leave `maskAllInputs` enabled unless you have a specific reason and a data-handling policy for unmasked inputs. # Queries & Tables Source: https://docs.hexclave.com/guides/apps/analytics/queries-and-tables Inspect raw analytics rows and run read-only ClickHouse SQL against your project's dataset The Analytics app stores your project's data in [ClickHouse](https://clickhouse.com/) and gives you two ways to explore it: * **Tables** - a point-and-click grid for browsing raw rows. Best for "what just happened?" triage. * **Queries** - a read-only ClickHouse SQL workspace for aggregation, joins, and reusable analysis. Both read from the same dataset and are automatically scoped to the current project and branch, so you never see another tenant's data and you never have to filter by `project_id` yourself. This guide covers how to *use* Tables and Queries. For enabling the app and capturing events from the SDK, see the [Analytics overview](./overview). ## What you can query Your dataset is exposed as a set of read-only views. Reference them directly by name (e.g. `events`) or fully-qualified (`default.events`) - both work. | Table | What it holds | | -------------------------- | ---------------------------------------------------------------------------------- | | `events` | Every analytics event (page views, clicks, token refreshes, sign-up rule triggers) | | `users` | One row per user, with profile, metadata, and restriction state | | `contact_channels` | Emails and other contact channels per user | | `teams` | Teams in your project | | `team_member_profiles` | Team membership profiles | | `team_permissions` | Team-scoped permissions granted to users | | `team_invitations` | Outstanding and historical team invites | | `project_permissions` | Project-scoped permissions granted to users | | `connected_accounts` | OAuth accounts linked to users | | `refresh_tokens` | Active and historical refresh tokens | | `email_outboxes` | Emails queued/sent by your project | | `notification_preferences` | Per-user notification category opt-in/out | Run `SHOW TABLES` or `DESCRIBE events` in the Queries workspace to discover columns at any time - both are allowed. ### The `events` table `events` is the table you'll use most. Its columns are: | Column | Type | Description | | --------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------- | | `event_type` | `LowCardinality(String)` | The event name, e.g. `$page-view`, `$click`, `$token-refresh`, `$sign-up-rule-trigger` | | `event_at` | `DateTime64(3, 'UTC')` | When the event occurred (UTC, millisecond precision) | | `data` | `JSON` | Event-specific payload (see [Working with the `data` payload](#working-with-the-data-payload)) | | `user_id` | `Nullable(String)` | The acting user, when authenticated | | `team_id` | `Nullable(String)` | Reserved; currently always `NULL` | | `refresh_token_id` | `Nullable(String)` | Session's refresh token, when applicable | | `session_replay_id` | `Nullable(String)` | Linked [session replay](./overview#session-replays), if one exists | | `session_replay_segment_id` | `Nullable(String)` | Replay segment that produced the event | | `project_id` / `branch_id` | `String` | Tenant identity - filtered automatically; you rarely need these | | `created_at` | `DateTime64(3, 'UTC')` | Ingestion time (usually a few ms after `event_at`) | `team_id` on `events` is reserved and currently always `NULL`. Don't build team-level analytics on it - join to `team_member_profiles` via `user_id` instead. ### Event types and their payloads The shape of `data` depends on `event_type`. The built-in event types are: | `event_type` | Captured by | Key `data` fields | | ----------------------- | ----------- | --------------------------------------------------------------------------------------------------------------- | | `$page-view` | Client SDK | `path`, `url`, `referrer`, `title`, `entry_type`, `viewport_width`, `viewport_height`, `user_agent` | | `$click` | Client SDK | `tag_name`, `text`, `href`, `selector`, `url`, `path`, `x`/`y` (and scaled variants) | | `$token-refresh` | Server | `refresh_token_id`, `is_anonymous`, `ip_info` (`{ ip, is_trusted, country_code, region_code, city_name, ... }`) | | `$sign-up-rule-trigger` | Server | `rule_id`, `action` (`allow` / `reject` / `restrict` / `log`), `email`, `auth_method`, `oauth_provider` | ## Tables Open **Analytics → Tables**, then pick a table from the sidebar (it opens on `events` by default). The grid shows every column the table returns, newest rows first. What you can do here: * **Search** - type in the filter to match text across all columns at once. * **Sort** - click any column header. Each table has a sensible default (e.g. `events` sorts by `event_at` descending). * **Toggle timestamps** - switch any date/time column between relative ("3 minutes ago") and absolute display from the **Columns** menu. * **Show/hide columns** - trim the grid to what you care about. * **Inspect a row** - click a row to open a detail dialog with every column and a pretty-printed view of the JSON `data` payload. * **Export** - download the current result set as CSV. Rows load incrementally as you scroll (50 at a time), so large tables stay responsive. Use Tables for fast incident triage; switch to Queries when you need to aggregate or correlate across rows. ## Queries Open **Analytics → Queries** to get a SQL editor. Write a query, run it, and the results appear in the same grid (with the same sorting, search, and CSV export as Tables). A minimal starting point: ```sql theme={null} SELECT * FROM events ORDER BY event_at DESC LIMIT 100 ``` ### Automatic project scoping Every query runs against your current project and branch only. Row-level security injects the tenant filter for you, so this: ```sql theme={null} SELECT event_type, count() AS c FROM events GROUP BY event_type ORDER BY c DESC ``` returns only *your* events - no `WHERE project_id = ...` required. Adding explicit tenant filters is harmless but unnecessary, and you cannot override the scoping to read other tenants' data. ### What's allowed The workspace is strictly read-only. You can run: * `SELECT` and `WITH` (CTEs) * `SHOW TABLES`, `SHOW GRANTS`, `DESCRIBE`, `EXPLAIN` Anything that writes or reaches outside the dataset is blocked, including `INSERT`, `UPDATE`, `DELETE`, `ALTER`, `CREATE`, `DROP`, `TRUNCATE`, multi-statement scripts, and table functions like `file()`, `url()`, `remote()`, and `s3()`. Most `system.*` tables are off-limits too (table/column metadata is the exception). ### Limits Queries run inside a budget so a single query can't overload the dataset: | Limit | Value | | --------------- | --------------------------------------------------------- | | Max result rows | 10,000 | | Max result size | 10 MiB | | Timeout | Up to your plan's cap (Free 10s · Team 60s · Growth 300s) | If you hit the row or byte cap the query fails rather than returning a partial result, so always scope with `WHERE`, aggregate, or add a `LIMIT`. If a query times out, narrow the time range (`event_at >= now() - INTERVAL 1 DAY`) or pre-aggregate. ### Parameterized queries Use ClickHouse's `{name:Type}` placeholders to keep values out of your SQL string and avoid escaping issues: ```sql theme={null} SELECT event_at, user_id, data.path FROM events WHERE event_type = {event_type:String} AND event_at >= now() - INTERVAL {days:UInt32} DAY ORDER BY event_at DESC LIMIT 100 ``` ### Working with the `data` payload `data` is a real ClickHouse `JSON` column, so you can reach into it with dot notation and cast as needed: ```sql theme={null} SELECT event_at, data.path AS path, data.referrer AS referrer FROM events WHERE event_type = '$page-view' AND data.path = '/pricing' ORDER BY event_at DESC LIMIT 50 ``` For values you want to treat as a specific type, cast explicitly: ```sql theme={null} SELECT CAST(data.email, 'Nullable(String)') AS email FROM events WHERE event_type = '$sign-up-rule-trigger' LIMIT 20 ``` To discover which keys exist in a payload, expand them: ```sql theme={null} SELECT arrayJoin(JSONExtractKeys(toString(data))) AS key, count() AS c FROM events WHERE event_type = '$click' GROUP BY key ORDER BY c DESC ``` ### Saving queries Save a query to reuse it later: queries live in **folders** in the sidebar. You can **Save** a new query, **Save As** to copy one, or overwrite the selected query after editing it. Selecting a saved query loads its SQL and runs it immediately. Deleting a folder removes the queries inside it. ## Examples ### Daily active users (last 7 days) ```sql theme={null} SELECT toDate(event_at) AS day, uniqExact(user_id) AS active_users FROM events WHERE event_at >= today() - INTERVAL 7 DAY AND user_id IS NOT NULL GROUP BY day ORDER BY day ``` ### Top pages by views (last 24 hours) ```sql theme={null} SELECT data.path AS path, count() AS views FROM events WHERE event_type = '$page-view' AND event_at >= now() - INTERVAL 1 DAY GROUP BY path ORDER BY views DESC LIMIT 20 ``` ### Event volume per hour ```sql theme={null} SELECT toStartOfHour(event_at) AS hour, event_type, count() AS events FROM events WHERE event_at >= now() - INTERVAL 1 DAY GROUP BY hour, event_type ORDER BY hour ``` ### Most-clicked elements ```sql theme={null} SELECT data.selector AS selector, data.text AS label, count() AS clicks FROM events WHERE event_type = '$click' AND event_at >= now() - INTERVAL 7 DAY GROUP BY selector, label ORDER BY clicks DESC LIMIT 25 ``` ### Recent sign-up rule rejections ```sql theme={null} SELECT event_at AS triggered_at, CAST(data.email, 'Nullable(String)') AS email, CAST(data.rule_id, 'Nullable(String)') AS rule_id FROM events WHERE event_type = '$sign-up-rule-trigger' AND CAST(data.action, 'Nullable(String)') = 'reject' ORDER BY event_at DESC LIMIT 100 ``` ### New users this week ```sql theme={null} SELECT id, primary_email, signed_up_at FROM users WHERE signed_up_at >= today() - INTERVAL 7 DAY ORDER BY signed_up_at DESC ``` ### Token refreshes by country ```sql theme={null} SELECT CAST(data.ip_info.country_code, 'Nullable(String)') AS country, count() AS refreshes FROM events WHERE event_type = '$token-refresh' AND event_at >= now() - INTERVAL 30 DAY GROUP BY country ORDER BY refreshes DESC ``` ## Tips & gotchas * **Events are eventually consistent.** New events are ingested asynchronously and can take a few seconds to appear. If a fresh event is missing, wait and re-run. * **Always bound your time range.** Filtering on `event_at` keeps queries fast and well under the result limits. * **Query the views, not internals.** Stick to the table names listed above (the `default.*` views). Internal physical tables aren't granted to the query runner and bypass the tenant safety policies. * **Metadata columns are strings.** On `users`, fields like `client_metadata` and `server_metadata` are stored as JSON-encoded `String`, so parse them with `JSONExtract*` functions rather than dot notation. * **Legacy rows may use camelCase keys.** Older `$sign-up-rule-trigger` rows can carry `ruleId` instead of `rule_id`; use `COALESCE` over both if you query far back in history. ## Related * [Analytics Overview](./overview) - enabling the app and capturing events from the SDK. * [Sign-up Rules](../authentication/sign-up-rules) - the source of `$sign-up-rule-trigger` events. # Replays & Clickmaps Source: https://docs.hexclave.com/guides/apps/analytics/replays-and-clickmaps Record real user sessions and visualize where users click Beyond raw events and SQL, Analytics ships two visual debugging tools: **Session Replays** (watch a real user's session play back) and **Clickmaps** (see where users actually click on a page). Both are sub-apps of Analytics - they turn on with the Analytics app and are captured automatically by the Hexclave client SDK, so there's no separate script tag to install. A **Clickmap** aggregates how many times each on-page element was clicked (with dead-click detection). Clicks are tied to DOM elements, not pixel coordinates. ## Requirements Replays and clickmaps both depend on the SDK's analytics capture, which runs when **all** of the following are true: 1. The **Analytics** app is enabled in your dashboard (**Apps -> Analytics**). The Session Replays and Clickmaps sub-apps inherit this - you don't enable them separately. 2. Your client app uses a **persistent `tokenStore`** (e.g. `"cookie"`, or `"nextjs-cookie"` in Next.js). Without one, the SDK does not start capture. 3. The visitor has a Hexclave **session with a refresh token** (a signed-in user, or an anonymous session created by the SDK). If there is no refresh token, the client may still attempt to send events, but the server will reject them and nothing is stored. When those hold, the SDK captures `$page-view` and `$click` events and (for replays) records the session - no manual setup required. ## Session Replays Session replays let you move from "an event happened" to "what the user actually saw." The SDK captures DOM snapshots and mutations, mouse interactions, and page state over time, then plays them back as a reconstruction of the session. ### Enabling and disabling Replay recording is **on by default** once the requirements above are met. You can tune or opt out of it through the `analytics` option when you create your client app: ```ts theme={null} import { HexclaveClientApp } from "@hexclave/js"; // replace `js` with the correct framework SDK package export const hexclaveClientApp = new HexclaveClientApp({ // ...your existing client app options tokenStore: "cookie", // use "nextjs-cookie" in Next.js analytics: { replays: { // Recording is enabled by default; set to false to opt out. enabled: true, // Privacy controls (see below) maskAllInputs: true, }, }, }); ``` To turn replays off entirely, pass `analytics: { replays: { enabled: false } }`. To stop **all** analytics capture (events and replays), pass `analytics: { enabled: false }`. ### Privacy controls Replays are masked by default to keep sensitive content out of recordings: | Option | Default | Effect | | --------------- | ------- | ------------------------------------------------------------------------ | | `maskAllInputs` | `true` | Masks the contents of `` fields so typed values aren't recorded | | `blockClass` | — | Block elements matching a CSS class name or `RegExp` from being recorded | | `blockSelector` | — | Block elements matching a CSS selector from being recorded | ```ts theme={null} analytics: { replays: { maskAllInputs: true, blockClass: "hx-private", blockSelector: "[data-private]", }, } ``` Leave `maskAllInputs` on unless you have a specific reason and a data-handling policy for unmasked input. Disabling it records exactly what users type into forms. ### Viewing replays Replays live under **Analytics -> Replays** in the dashboard. The list shows each session's user, start and last-activity time, duration, and event/click counts. You can filter by: * User and team * Duration (min/max) * Last active (24h / 7d / 30d) * Minimum click count Open a replay to play it back. The player supports: * Play / pause and scrubbing * Playback speeds of **0.5×, 1×, 2×, and 4×** * **Skip inactivity** (on by default) to jump past idle gaps * Timeline markers for clicks and page views * Multi-tab playback, with an optional "follow active tab" * A shareable deep link to the exact replay You can also see a user's replays directly from their profile under the **Session Replays** tab on the user detail page. ### Storage and limits Recorded replay data is stored privately (the DOM recording is gzipped and kept in object storage), with session metadata in the database. A session groups activity from the same login, and is closed after **3 minutes** of inactivity or **12 hours** total. Plans include a monthly allowance of new replays; once the allowance is exhausted, new recordings are skipped until the next cycle. ## Clickmaps A clickmap overlays your live site with the **number of clicks each element received**, so you can see what users interact with - and spot "dead clicks" on things that look clickable but aren't. Clicks are tied to DOM elements (not pixel coordinates): a marker appearing in the middle of an element does not mean the user clicked that exact point. Click data comes from the same `$click` events the SDK already captures, so there's nothing extra to instrument. ### Enabling Clickmaps need only the Analytics app enabled and the SDK capturing clicks (the [requirements](#requirements) above). Unlike replays, the clickmap is rendered **on your own site** as an overlay, not inside the dashboard. To activate it: 1. Go to **Analytics -> Clickmaps** in the dashboard. 2. Add your site's origin as a trusted domain and generate an overlay token (valid for **24 hours**). 3. Paste the provided console snippet into your browser's dev console while on your site. The overlay then appears on the page, reading aggregated click data for the current URL. ### Controls From the overlay you can adjust: * **Date range** - 24h, 7d, or 30d (queries span at most 31 days) * **Device / viewport** - all, mobile, tablet, laptop, desktop, widescreen, or TV * **URL pattern** - target specific pages with `*` wildcards * **Element search** - find a specific element * **Dead clicks** - show or hide clicks that produced no page response ## How this relates to your events These features build on the same analytics pipeline described in [Queries & Tables](./queries-and-tables): * **`$click` and `$page-view` events** land in the `events` table (ClickHouse). Clickmaps read from a derived table populated automatically from `$click` events. * **Session replay recordings** are stored separately (object storage + database), not in ClickHouse. Your analytics events carry a `session_replay_id` so you can connect an event row back to the replay it occurred in. ## Related * [Analytics overview](./overview) - enabling Analytics and what gets tracked * [Queries & Tables](./queries-and-tables) - the event schema, SQL runner, and Tables view # API Keys Source: https://docs.hexclave.com/guides/apps/api-keys/overview Create and manage API keys for users and teams The API Keys app enables your users to generate and manage API keys for programmatic access to your backend services. API keys provide a secure way to authenticate requests, allowing developers to associate API calls with specific **users** or **teams**. Hexclave provides prebuilt UI components so users and teams can manage their own keys. ## Concepts ### The authentication flow A user or client sends an API request with an API key to your application server. Your server validates the API key with Hexclave, which returns an authenticated `User` (or `Team`) object. Your server then processes the request and returns the response. ### Two kinds of API keys Hexclave supports two kinds of API keys: * **User API keys** - associated with an individual user; calls authenticated with this key act on behalf of that user. * **Team API keys** - associated with a team; calls authenticated with this key act on behalf of that team. Only users with the `$manage_api_keys` permission for the team can create or revoke them. **Don't confuse API Keys with Project Keys.** The **API Keys app** documented here lets your end users issue keys for *their* accounts and teams. If you want to create or rotate the publishable / secret keys that *your* Hexclave project uses to call the Hexclave API, that lives in **Project Settings → Project Keys** instead. #### User API keys User API keys are associated with individual users and allow them to authenticate with your API. ```typescript title="app/components/create-api-key.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function CreateApiKey() { const user = useUser({ or: 'redirect' }); const handleCreateKey = async () => { const apiKey = await user.createApiKey({ description: "My client application", expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days }); console.log("API Key created:", apiKey.value); }; return ; } ``` ```typescript title="app/components/create-api-key.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateApiKey() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const apiKey = await user.createApiKey({ description: "Admin-provisioned API key", expiresAt: new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)), // 30 days }); return
API Key: {apiKey.value}
; } ```
```typescript title="components/CreateApiKey.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function CreateApiKey() { const user = useUser({ or: 'redirect' }); const handleCreateKey = async () => { const apiKey = await user.createApiKey({ description: "My client application", expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days }); console.log("API Key created:", apiKey.value); }; return ; } ``` ```python title="views.py" theme={null} import requests from django.http import JsonResponse def create_user_api_key(request): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # Create API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'user_id': 'me', 'description': 'My client application', 'expires_at_millis': int((time.time() + 90 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise Exception(f"Failed to create API key: {response.text}") return JsonResponse(response.json()) ``` ```python title="main.py" theme={null} import requests import time from fastapi import Cookie, HTTPException @app.post("/api/create-user-api-key") async def create_user_api_key(hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # Create API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, }, json={ 'user_id': 'me', 'description': 'My client application', 'expires_at_millis': int((time.time() + 90 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) return response.json() ``` ```python title="app.py" theme={null} import requests import time from flask import request, jsonify @app.route('/api/create-user-api-key', methods=['POST']) def create_user_api_key(): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # Create API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'user_id': 'me', 'description': 'My client application', 'expires_at_millis': int((time.time() + 90 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: return jsonify({'error': response.text}), response.status_code return jsonify(response.json()) ```
#### Team API keys Team API keys are associated with teams and can be used to provide access to team resources over your API. ```typescript title="app/components/create-team-api-key.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function CreateTeamApiKey({ teamId }: { teamId: string }) { const user = useUser({ or: 'redirect' }); const team = user.useTeam(teamId); const handleCreateKey = async () => { if (!team) return; const teamApiKey = await team.createApiKey({ description: "Team integration service", expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days }); console.log("Team API Key created:", teamApiKey.value); }; return ; } ``` ```typescript title="app/components/create-team-api-key.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateTeamApiKey({ teamId }: { teamId: string }) { const team = await hexclaveServerApp.getTeam(teamId); if (!team) { return
Team not found
; } const teamApiKey = await team.createApiKey({ description: "Admin-provisioned team API key", expiresAt: new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)), // 30 days }); return
Team API Key: {teamApiKey.value}
; } ```
```typescript title="components/CreateTeamApiKey.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function CreateTeamApiKey({ teamId }: { teamId: string }) { const user = useUser({ or: 'redirect' }); const team = user.useTeam(teamId); const handleCreateKey = async () => { if (!team) return; const teamApiKey = await team.createApiKey({ description: "Team integration service", expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days }); console.log("Team API Key created:", teamApiKey.value); }; return ; } ``` ```python title="views.py" theme={null} import requests import time from django.http import JsonResponse def create_team_api_key(request, team_id): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # Create team API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'team_id': team_id, 'description': 'Team integration service', 'expires_at_millis': int((time.time() + 60 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise Exception(f"Failed to create team API key: {response.text}") return JsonResponse(response.json()) ``` ```python title="main.py" theme={null} import requests import time from fastapi import Cookie, HTTPException @app.post("/api/teams/{team_id}/api-keys") async def create_team_api_key(team_id: str, hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # Create team API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, }, json={ 'team_id': team_id, 'description': 'Team integration service', 'expires_at_millis': int((time.time() + 60 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) return response.json() ``` ```python title="app.py" theme={null} import requests import time from flask import request, jsonify @app.route('/api/teams//api-keys', methods=['POST']) def create_team_api_key(team_id): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # Create team API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'team_id': team_id, 'description': 'Team integration service', 'expires_at_millis': int((time.time() + 60 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: return jsonify({'error': response.text}), response.status_code return jsonify(response.json()) ```
## Enabling the API Keys App To use API keys in your application, you must enable the **API Keys** app in your Hexclave dashboard: 1. Open your Hexclave dashboard 2. Go to **Apps** 3. Find and open **API Keys** 4. Click **Enable** ### Dashboard settings Once enabled, the API Keys app exposes exactly two toggles under **API Key Settings**: | Setting | Config field | Description | | ----------------- | ---------------------- | ---------------------------------------------------------------------------------------------- | | **User API Keys** | `apiKeys.enabled.user` | Allow users to create API keys for their accounts. Enables the `user-api-keys` backend routes. | | **Team API Keys** | `apiKeys.enabled.team` | Allow users to create API keys for their teams. Enables the `team-api-keys` backend routes. | Both are **disabled by default**. Changes require clicking **Save** before they take effect. Toggling **User API Keys** controls whether the `` component shows its API Keys tab. Toggling **Team API Keys** controls whether the team settings page shows its API Keys section to users with the `$manage_api_keys` permission. ### Team permission requirement Creating, listing, and revoking **team** API keys requires the [`$manage_api_keys`](/guides/apps/rbac/overview) permission on the team. Make sure your team roles grant this permission to the right members (e.g. admins). ## Prebuilt UI Components Hexclave provides prebuilt UI components that let your users manage their own API keys without any additional code. ### User API Keys UI For frameworks that support React components, the `` component includes an API Keys tab where users can: * View all their active API keys * Create new API keys with a description and an expiration date * Revoke existing API keys * See when each key was created and when it expires The tab is only shown when `apiKeys.enabled.user` is on for your project. ```typescript title="app/account/page.tsx" theme={null} import { AccountSettings } from '@hexclave/next'; export default function MyAccountPage() { return ; } ``` ```typescript title="src/account-page.tsx" theme={null} import { AccountSettings } from '@hexclave/react'; export default function MyAccountPage() { return ; } ``` ### Team API Keys UI The team settings page automatically includes an **API Keys** section when **all** of the following are true: * The API Keys app is enabled * `apiKeys.enabled.team` is on for your project * The current user has the `$manage_api_keys` permission on the team Users with the right permission can create, list, and revoke team API keys directly from the team settings interface - no extra code required. ## The `ApiKey` object Both `user.listApiKeys()` and `team.listApiKeys()` return arrays of `ApiKey` objects. The same shape comes back from `user.createApiKey(...)` / `team.createApiKey(...)`, except the `value` is the full plaintext key only on the first view. | Field | Type | Description | | ------------------- | ----------------------------------------------- | ----------------------------------------------------------------- | | `id` | `string` | Stable identifier for the key | | `description` | `string` | Human-readable description set on creation | | `createdAt` | `Date` | When the key was created | | `expiresAt` | `Date \| undefined` | Optional expiration timestamp | | `manuallyRevokedAt` | `Date \| null \| undefined` | Set when the key was explicitly revoked | | `value` | `string` (first view) / `{ lastFour: string }` | Full key on first view only, then just the last 4 characters | | `type` | `"user" \| "team"` | Which flavor of key this is | | `userId` / `teamId` | `string` | The owning user (for `type: "user"`) or team (for `type: "team"`) | | `update(options)` | `(options) => Promise` | Update `description`, `expiresAt`, or `revoked` | | `revoke()` | `() => Promise` | Convenience for `update({ revoked: true })` | | `isValid()` | `() => boolean` | `true` if the key is not expired and not manually revoked | | `whyInvalid()` | `() => "manually-revoked" \| "expired" \| null` | Reason the key is invalid, or `null` if it's still valid | The full plaintext value of an API key is **only returned once** - at creation time. After that, the SDK only ever exposes `value.lastFour`. Display, copy, or store the value immediately on creation; it cannot be retrieved later. ### `isPublic` keys When creating a key, pass `isPublic: true` to exempt it from Hexclave's secret scanner. The secret scanner automatically revokes API keys it detects in public places (e.g. exposed in a GitHub repo). Use `isPublic` only for keys that are intentionally exposed to clients (e.g. anonymous-style access tokens). ## Working with API Keys ### Creating a user API key ```typescript title="app/components/create-api-key.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function CreateApiKey() { const user = useUser({ or: 'redirect' }); const handleCreateKey = async () => { const apiKey = await user.createApiKey({ description: "My client application", expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days }); console.log("API Key created:", apiKey.value); }; return ; } ``` ```typescript title="app/components/create-api-key.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateApiKey() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const apiKey = await user.createApiKey({ description: "Admin-provisioned API key", expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days }); return
API Key: {apiKey.value}
; } ```
```typescript title="components/CreateApiKey.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function CreateApiKey() { const user = useUser({ or: 'redirect' }); const handleCreateKey = async () => { const apiKey = await user.createApiKey({ description: "My client application", expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), }); console.log("API Key created:", apiKey.value); }; return ; } ``` ```python title="views.py" theme={null} import requests import time from django.http import JsonResponse def create_user_api_key(request): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # Create API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'user_id': 'me', 'description': 'My client application', 'expires_at_millis': int((time.time() + 90 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise Exception(f"Failed to create API key: {response.text}") return JsonResponse(response.json()) ``` ```python title="main.py" theme={null} import requests import time from fastapi import Cookie, HTTPException @app.post("/api/create-user-api-key") async def create_user_api_key(hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # Create API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, }, json={ 'user_id': 'me', 'description': 'My client application', 'expires_at_millis': int((time.time() + 90 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) return response.json() ``` ```python title="app.py" theme={null} import requests import time from flask import request, jsonify @app.route('/api/create-user-api-key', methods=['POST']) def create_user_api_key(): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # Create API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'user_id': 'me', 'description': 'My client application', 'expires_at_millis': int((time.time() + 90 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: return jsonify({'error': response.text}), response.status_code return jsonify(response.json()) ```
### Creating a team API key Requires the `$manage_api_keys` team permission. ```typescript title="app/components/create-team-api-key.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function CreateTeamApiKey({ teamId }: { teamId: string }) { const user = useUser({ or: 'redirect' }); const team = user.useTeam(teamId); const handleCreateKey = async () => { if (!team) return; const teamApiKey = await team.createApiKey({ description: "Team integration service", expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days }); console.log("Team API Key created:", teamApiKey.value); }; return ; } ``` ```typescript title="app/components/create-team-api-key.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateTeamApiKey({ teamId }: { teamId: string }) { const team = await hexclaveServerApp.getTeam(teamId); if (!team) { return
Team not found
; } const teamApiKey = await team.createApiKey({ description: "Admin-provisioned team API key", expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days }); return
Team API Key: {teamApiKey.value}
; } ```
```typescript title="components/CreateTeamApiKey.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function CreateTeamApiKey({ teamId }: { teamId: string }) { const user = useUser({ or: 'redirect' }); const team = user.useTeam(teamId); const handleCreateKey = async () => { if (!team) return; const teamApiKey = await team.createApiKey({ description: "Team integration service", expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days }); console.log("Team API Key created:", teamApiKey.value); }; return ; } ``` ```python title="views.py" theme={null} import requests import time from django.http import JsonResponse def create_team_api_key(request, team_id): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # Create team API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'team_id': team_id, 'description': 'Team integration service', 'expires_at_millis': int((time.time() + 60 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise Exception(f"Failed to create team API key: {response.text}") return JsonResponse(response.json()) ``` ```python title="main.py" theme={null} import requests import time from fastapi import Cookie, HTTPException @app.post("/api/teams/{team_id}/api-keys") async def create_team_api_key(team_id: str, hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # Create team API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, }, json={ 'team_id': team_id, 'description': 'Team integration service', 'expires_at_millis': int((time.time() + 60 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) return response.json() ``` ```python title="app.py" theme={null} import requests import time from flask import request, jsonify @app.route('/api/teams//api-keys', methods=['POST']) def create_team_api_key(team_id): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # Create team API key via client API response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'team_id': team_id, 'description': 'Team integration service', 'expires_at_millis': int((time.time() + 60 * 24 * 60 * 60) * 1000), } ) if response.status_code != 200: return jsonify({'error': response.text}), response.status_code return jsonify(response.json()) ```
### Listing API keys ```typescript title="app/components/api-keys-list.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function ApiKeysList() { const user = useUser({ or: 'redirect' }); const apiKeys = user.useApiKeys(); return (

Your API Keys

{apiKeys.map(key => (

{key.description}

Last 4 digits: {key.value.lastFour}

Created: {key.createdAt.toLocaleDateString()}

))}
); } ```
```typescript title="app/components/api-keys-list.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function ApiKeysList() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const apiKeys = await user.listApiKeys(); return (

Your API Keys

{apiKeys.map(key => (

{key.description}

Last 4 digits: {key.value.lastFour}

Created: {key.createdAt.toLocaleDateString()}

))}
); } ```
```typescript title="components/ApiKeysList.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function ApiKeysList() { const user = useUser({ or: 'redirect' }); const apiKeys = user.useApiKeys(); return (

Your API Keys

{apiKeys.map(key => (

{key.description}

Last 4 digits: {key.value.lastFour}

Created: {key.createdAt.toLocaleDateString()}

))}
); } ```
```python title="views.py" theme={null} import requests from django.http import JsonResponse def list_user_api_keys(request): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # List user's API keys via client API response = requests.get( 'https://api.hexclave.com/api/v1/user-api-keys?user_id=me', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, } ) if response.status_code != 200: raise Exception(f"Failed to list API keys: {response.text}") return JsonResponse(response.json(), safe=False) ``` ```python title="main.py" theme={null} import requests from fastapi import Cookie, HTTPException @app.get("/api/user-api-keys") async def list_user_api_keys(hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # List user's API keys via client API response = requests.get( 'https://api.hexclave.com/api/v1/user-api-keys?user_id=me', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, } ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) return response.json() ``` ```python title="app.py" theme={null} import requests from flask import request, jsonify @app.route('/api/user-api-keys', methods=['GET']) def list_user_api_keys(): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # List user's API keys via client API response = requests.get( 'https://api.hexclave.com/api/v1/user-api-keys?user_id=me', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, } ) if response.status_code != 200: return jsonify({'error': response.text}), response.status_code return jsonify(response.json()) ```
The same pattern works for team API keys via `team.useApiKeys()` (client) or `team.listApiKeys()` (server), and the `/api/v1/team-api-keys` REST endpoint. ### Validating an incoming API key on your server This is the core authentication flow: an incoming request includes an API key, and your server needs to know **which user or team** it represents. Pass the plaintext key directly to `hexclaveServerApp.getUser({ apiKey })` or `hexclaveServerApp.getTeam({ apiKey })` - Hexclave validates it and returns the corresponding object, or `null` if the key is invalid, expired, or revoked. ```typescript title="app/api/protected/route.ts" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export async function GET(request: Request) { const auth = request.headers.get("authorization"); const apiKey = auth?.replace(/^Bearer\s+/i, ""); if (!apiKey) { return new Response("Missing API key", { status: 401 }); } const user = await hexclaveServerApp.getUser({ apiKey }); if (!user) { return new Response("Invalid API key", { status: 401 }); } return Response.json({ userId: user.id, displayName: user.displayName }); } ``` ```typescript title="app/api/team-protected/route.ts" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export async function GET(request: Request) { const apiKey = request.headers.get("authorization")?.replace(/^Bearer\s+/i, ""); if (!apiKey) return new Response("Missing API key", { status: 401 }); const team = await hexclaveServerApp.getTeam({ apiKey }); if (!team) return new Response("Invalid team API key", { status: 401 }); return Response.json({ teamId: team.id, displayName: team.displayName }); } ``` ```python title="views.py" theme={null} import requests from django.http import JsonResponse def protected_view(request): auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Bearer '): return JsonResponse({'error': 'Missing API key'}, status=401) api_key = auth_header[len('Bearer '):] # Check the API key via server API check = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys/check', headers={ 'x-hexclave-access-type': 'server', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, json={'api_key': api_key}, ) if check.status_code != 200: return JsonResponse({'error': 'Invalid API key'}, status=401) api_key_obj = check.json() # Fetch the owning user user_resp = requests.get( f'https://api.hexclave.com/api/v1/users/{api_key_obj["user_id"]}', headers={ 'x-hexclave-access-type': 'server', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, ) user = user_resp.json() return JsonResponse({'userId': user['id'], 'displayName': user.get('display_name')}) ``` ```python title="main.py" theme={null} import requests from fastapi import Header, HTTPException @app.get("/api/protected") async def protected_view(authorization: str = Header(None)): if not authorization or not authorization.startswith('Bearer '): raise HTTPException(status_code=401, detail="Missing API key") api_key = authorization[len('Bearer '):] # Check the API key via server API check = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys/check', headers={ 'x-hexclave-access-type': 'server', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, json={'api_key': api_key}, ) if check.status_code != 200: raise HTTPException(status_code=401, detail="Invalid API key") api_key_obj = check.json() user_resp = requests.get( f'https://api.hexclave.com/api/v1/users/{api_key_obj["user_id"]}', headers={ 'x-hexclave-access-type': 'server', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, ) user = user_resp.json() return {'userId': user['id'], 'displayName': user.get('display_name')} ``` ```python title="app.py" theme={null} import requests from flask import request, jsonify @app.route('/api/protected', methods=['GET']) def protected_view(): auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Bearer '): return jsonify({'error': 'Missing API key'}), 401 api_key = auth_header[len('Bearer '):] # Check the API key via server API check = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys/check', headers={ 'x-hexclave-access-type': 'server', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, json={'api_key': api_key}, ) if check.status_code != 200: return jsonify({'error': 'Invalid API key'}), 401 api_key_obj = check.json() user_resp = requests.get( f'https://api.hexclave.com/api/v1/users/{api_key_obj["user_id"]}', headers={ 'x-hexclave-access-type': 'server', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, ) user = user_resp.json() return jsonify({'userId': user['id'], 'displayName': user.get('display_name')}) ``` ### Checking an existing key's validity When you already hold an `ApiKey` object (e.g. from `useApiKeys()`), use its synchronous helpers `isValid()` and `whyInvalid()`. The latter returns `"manually-revoked"`, `"expired"`, or `null`. ```typescript title="app/components/check-api-key.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function CheckApiKeyValidity({ apiKeyId }: { apiKeyId: string }) { const user = useUser({ or: 'redirect' }); const apiKeys = user.useApiKeys(); const apiKey = apiKeys.find(key => key.id === apiKeyId); if (!apiKey) { return
API key not found
; } if (apiKey.isValid()) { return
API key is valid
; } const reason = apiKey.whyInvalid(); return
API key is invalid: {reason}
; } ```
```typescript title="app/components/check-api-key.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CheckApiKeyValidity({ userId, apiKeyId }: { userId: string, apiKeyId: string }) { const user = await hexclaveServerApp.getUser(userId); if (!user) return
User not found
; const apiKeys = await user.listApiKeys(); const apiKey = apiKeys.find(key => key.id === apiKeyId); if (!apiKey) { return
API key not found
; } if (apiKey.isValid()) { return
API key is valid
; } const reason = apiKey.whyInvalid(); return
API key is invalid: {reason}
; } ```
```typescript title="components/CheckApiKey.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function CheckApiKeyValidity({ apiKeyId }: { apiKeyId: string }) { const user = useUser({ or: 'redirect' }); const apiKeys = user.useApiKeys(); const apiKey = apiKeys.find(key => key.id === apiKeyId); if (!apiKey) { return
API key not found
; } if (apiKey.isValid()) { return
API key is valid
; } const reason = apiKey.whyInvalid(); return
API key is invalid: {reason}
; } ```
```python title="views.py" theme={null} import requests import time from django.http import JsonResponse def check_api_key_validity(request, api_key_id): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # Get API key details via client API response = requests.get( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, } ) if response.status_code != 200: return JsonResponse({'error': 'API key not found'}, status=404) api_key = response.json() # Check if manually revoked if api_key.get('manually_revoked_at_millis'): return JsonResponse({ 'valid': False, 'reason': 'manually-revoked' }) # Check if expired if api_key.get('expires_at_millis'): if api_key['expires_at_millis'] < time.time() * 1000: return JsonResponse({ 'valid': False, 'reason': 'expired' }) return JsonResponse({'valid': True}) ``` ```python title="main.py" theme={null} import requests import time from fastapi import Cookie, HTTPException @app.get("/api/check-api-key/{api_key_id}") async def check_api_key_validity(api_key_id: str, hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # Get API key details via client API response = requests.get( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, } ) if response.status_code != 200: raise HTTPException(status_code=404, detail="API key not found") api_key = response.json() # Check if manually revoked if api_key.get('manually_revoked_at_millis'): return { 'valid': False, 'reason': 'manually-revoked' } # Check if expired if api_key.get('expires_at_millis'): if api_key['expires_at_millis'] < time.time() * 1000: return { 'valid': False, 'reason': 'expired' } return {'valid': True} ``` ```python title="app.py" theme={null} import requests import time from flask import request, jsonify @app.route('/api/check-api-key/', methods=['GET']) def check_api_key_validity(api_key_id): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # Get API key details via client API response = requests.get( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, } ) if response.status_code != 200: return jsonify({'error': 'API key not found'}), 404 api_key = response.json() # Check if manually revoked if api_key.get('manually_revoked_at_millis'): return jsonify({ 'valid': False, 'reason': 'manually-revoked' }) # Check if expired if api_key.get('expires_at_millis'): if api_key['expires_at_millis'] < time.time() * 1000: return jsonify({ 'valid': False, 'reason': 'expired' }) return jsonify({'valid': True}) ```
### Revoking an API key API keys can be revoked when they are no longer needed or if they have been compromised. Revoking is irreversible: a revoked key's `manuallyRevokedAt` becomes set and `isValid()` returns `false` (`whyInvalid()` returns `"manually-revoked"`). ```typescript title="app/components/revoke-api-key.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; export default function RevokeApiKey({ apiKeyId }: { apiKeyId: string }) { const user = useUser({ or: 'redirect' }); const apiKeys = user.useApiKeys(); const handleRevoke = async () => { const apiKeyToRevoke = apiKeys.find(key => key.id === apiKeyId); if (apiKeyToRevoke) { await apiKeyToRevoke.revoke(); console.log("API Key revoked"); } }; return ; } ``` ```typescript title="lib/api-keys.ts" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export async function revokeApiKey(userId: string, apiKeyId: string) { const user = await hexclaveServerApp.getUser(userId); if (!user) return; const apiKeys = await user.listApiKeys(); const apiKeyToRevoke = apiKeys.find(key => key.id === apiKeyId); if (apiKeyToRevoke) { await apiKeyToRevoke.revoke(); } } ``` ```typescript title="components/RevokeApiKey.tsx" theme={null} "use client"; import { useUser } from "@hexclave/react"; export default function RevokeApiKey({ apiKeyId }: { apiKeyId: string }) { const user = useUser({ or: 'redirect' }); const apiKeys = user.useApiKeys(); const handleRevoke = async () => { const apiKeyToRevoke = apiKeys.find(key => key.id === apiKeyId); if (apiKeyToRevoke) { await apiKeyToRevoke.revoke(); console.log("API Key revoked"); } }; return ; } ``` ```python title="views.py" theme={null} import requests from django.http import JsonResponse def revoke_api_key(request, api_key_id): # Get the current user's access token from session/cookie access_token = request.COOKIES.get('hexclave-access-token') # Revoke API key via client API (update with revoked: true) response = requests.patch( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'revoked': True, } ) if response.status_code != 200: raise Exception(f"Failed to revoke API key: {response.text}") return JsonResponse({'message': 'API key revoked successfully'}) ``` ```python title="main.py" theme={null} import requests from fastapi import Cookie, HTTPException @app.delete("/api/user-api-keys/{api_key_id}") async def revoke_api_key(api_key_id: str, hexclave_access_token: str = Cookie(None, alias="hexclave-access-token")): if not hexclave_access_token: raise HTTPException(status_code=401, detail="Not authenticated") # Revoke API key via client API (update with revoked: true) response = requests.patch( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': hexclave_access_token, }, json={ 'revoked': True, } ) if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail=response.text) return {"message": "API key revoked successfully"} ``` ```python title="app.py" theme={null} import requests from flask import request, jsonify @app.route('/api/user-api-keys/', methods=['DELETE']) def revoke_api_key(api_key_id): access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 # Revoke API key via client API (update with revoked: true) response = requests.patch( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ 'x-hexclave-access-type': 'client', 'x-hexclave-project-id': hexclave_project_id, 'x-hexclave-publishable-client-key': hexclave_publishable_client_key, 'x-hexclave-access-token': access_token, }, json={ 'revoked': True, } ) if response.status_code != 200: return jsonify({'error': response.text}), response.status_code return jsonify({'message': 'API key revoked successfully'}) ``` ## Best Practices 1. **Show the value once.** API key values are returned in plaintext only at creation. Always display, copy, or send them immediately - don't expect to fetch them again later. 2. **Set sensible expirations.** Long-lived keys are convenient but risky. Default to short expirations (30–90 days) and let users rotate. 3. **Don't share user keys across users.** A user API key acts as that exact user. If a service needs to act independently, prefer a team API key with a service-style role. 4. **Use `$manage_api_keys` deliberately.** Only grant this team permission to roles you'd trust to lock or unlock the entire team's programmatic access. 5. **Mark public keys with `isPublic: true`.** This opts them out of the secret scanner so your legitimately-public keys don't get auto-revoked. 6. **Use `getUser({ apiKey })` / `getTeam({ apiKey })` for validation.** Never try to parse or compare the plaintext value yourself - Hexclave handles hashing, expiration, and revocation. # All Auth Providers Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers Configure authentication providers for your application Hexclave supports a variety of authentication providers to give your users flexible sign-in options. You can configure these providers through the Hexclave dashboard. ## Overview Authentication providers determine how users can sign in to your application. Hexclave supports the following provider types: * **Email/Password**: Traditional email and password authentication * **Magic Link**: Passwordless authentication via email links * **OAuth Providers**: Third-party sign-in with providers like Google, GitHub, Facebook, Microsoft, and more * **Custom OIDC**: Bring any OpenID Connect-compliant identity provider (Okta, Auth0, Keycloak, and others) * **Passkeys**: WebAuthn-based passwordless authentication ## Configuring Providers Navigate to your project in the Hexclave dashboard. Click on the **Auth Providers** section in the sidebar. Toggle the providers you want to enable for your application. For OAuth providers, you can use Hexclave's shared keys for development or configure your own OAuth client ID and client secret for production. ## Shared vs. Custom OAuth Keys For development and testing, Hexclave provides shared OAuth keys that work out of the box. For production, you should set up your own OAuth client credentials. Custom OIDC providers always use your own credentials. A [development environment](/guides/going-further/local-vs-cloud-dashboard) typically uses shared keys. Configure your own client ID and secret in the [cloud dashboard](https://app.hexclave.com) before you launch. ### Shared Keys Shared keys allow you to quickly get started without needing to register your application with each OAuth provider. These are suitable for development only. ### Custom Keys For production use, configure your own OAuth client ID and client secret for each provider: 1. Register your application with the OAuth provider (e.g., Google Cloud Console, GitHub Developer Settings) 2. Obtain the client ID and client secret 3. Enter them in the Hexclave dashboard under the respective provider settings ## OAuth Providers Sign in with GitHub Sign in with Google Sign in with Facebook Sign in with Microsoft Sign in with Spotify Sign in with Discord Sign in with GitLab Sign in with Apple Sign in with Bitbucket Sign in with LinkedIn Sign in with X Sign in with Twitch ## Other Authentication Methods These aren't OAuth providers, but round out how users sign in and secure their accounts. WebAuthn-based passwordless authentication TOTP-based two-factor authentication Bring any OpenID Connect identity provider (Okta, Auth0, Keycloak, and more). Team plan or above. ## Going to Production When preparing your application for production, make sure to: * Replace shared OAuth keys with your own client credentials * Configure a custom email server for email-based authentication * Set up proper redirect URLs for OAuth providers For more details, see [Going to Production](/guides/apps/launch-checklist/overview). # Apple Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/apple Set up Apple as an authentication provider with Hexclave This guide explains how to set up Apple as an authentication provider with Hexclave. Sign in with Apple allows users to sign in to your application using their Apple ID. You will need to create an Apple Developer account, and generate an Apple Services ID, Apple Private Key, Apple Team ID, and Apple Key ID. ## Integration Steps ### Create an Apple App ID and Services ID 1. Log in to the [Apple Developer Portal](https://developer.apple.com/). 2. Navigate to **Certificates, IDs & Profiles**. 3. In the sidebar, select **Identifiers** and click the "+" button to register a new identifier. 4. Select **App IDs** and click **Continue**. 5. Select **App** as the type and click **Continue**. 6. Give your app a description and a Bundle ID (e.g., com.yourdomain.app). 7. Scroll down and enable **Sign in with Apple**, then click **Continue**, then **Register**. 8. In the top-right of the Identifiers page, switch to **Services IDs**. 9. Click the "+" button to create a new Service ID and click **Continue**. 10. Give it a description and an identifier (note: this cannot be the same as your App ID's bundle ID). 11. Click **Continue**, then **Register**. 12. From the list, select your new Service ID. 13. Enable **Sign in with Apple** by checking the box. 14. Click **Configure** next to Sign in with Apple. 15. Register your domains (add api.hexclave.com). 16. Add the return URL: `https://api.hexclave.com/api/v1/auth/oauth/callback/apple` 17. Click **Done**, then **Continue**, and then **Save**. ### Create a Private Key 1. In the sidebar, select **Keys** and click the "+" button. 2. Give your key a name and usage description. 3. Scroll down to enable **Sign in with Apple** and click **Configure**. 4. Select your Primary App ID that you created earlier and click **Save**. 5. Click **Continue**, then **Register**. 6. On the next page, **download your key file (.p8)**. This is critical as you won't be able to download it again. 7. Note your **Key ID** displayed on this page. 8. Click **Done**. 9. Find your **Account ID** at the very top-right of the Apple Developer Portal page. ### Configure Key-Based Credentials (Recommended) In the Apple provider dialog, choose the **Automatic (Recommended)** tab. Hexclave mints a short-lived client secret at token-exchange time from your Apple credentials. Enter: * **Team ID**: Your Apple Developer account ID found at the top-right of the portal * **Key ID**: The ID of the private key you just created * **Private key**: The contents of the `.p8` file you downloaded Use the identifier of your Apple Services ID as the **Client ID**. Keep the `.p8` contents private. ### Enable Apple OAuth in Hexclave 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Apple** as the provider. 3. Set the **Service ID (Client ID)** to your Services ID identifier, then choose the **Automatic (Recommended)** tab and enter the Team ID, Key ID, and `.p8` private key contents. ### Legacy: Static Client Secret Apple also supports a manually generated client-secret JWT. In the Apple provider dialog, choose the **Client secret (Legacy)** tab. This is retained for existing integrations, but is less convenient because it expires (Apple allows a maximum lifetime of six months) and must be rotated manually. Enter it in **Client Secret** instead of the key-based credentials. Saving on this tab clears any stored Team ID, Key ID, and private key. ## Native App Configuration (iOS/macOS) Native iOS and macOS apps using the Hexclave Swift SDK require Bundle ID configuration in addition to the web OAuth setup above. Native apps use Apple's native Sign in with Apple flow (`ASAuthorizationController`) instead of web-based OAuth. Bundle IDs are only required for native iOS/macOS apps. Web applications only need the Service ID configuration described above. ### Add Your Bundle IDs 1. On the Hexclave dashboard, navigate to **Auth Methods** and select your Apple provider. 2. In the Apple configuration modal, add your app's **Bundle ID** (e.g., `com.yourdomain.app`). This is the same Bundle ID from your App ID in Apple Developer Portal (Step 1 above). 3. If you have multiple apps (e.g., separate iOS and macOS apps), add all their Bundle IDs. 4. Click **Save**. Your native app can now use `signInWithOAuth(provider: "apple")` from the Swift SDK. ### Need More Help? * Check the [Sign in with Apple Documentation](https://developer.apple.com/sign-in-with-apple/get-started/) * Join our [Discord](https://discord.hexclave.com) # Bitbucket Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/bitbucket Set up Bitbucket as an authentication provider with Hexclave This guide explains how to set up Bitbucket as an authentication provider with Hexclave. Bitbucket OAuth allows users to sign in to your application using their Bitbucket account. ## Integration Steps 1. Log in to your [Bitbucket Workspaces account](https://bitbucket.org/account/workspaces/). 2. Under **Workspaces**, find your workspace and select **Manage**. 3. In the left sidebar, scroll down and select **OAuth consumers**. 4. Click **Add consumer**. 5. Fill out the form with the following details: * **Name**: Choose a name for your application * **Description**: Add a brief description of your application * **Callback URL**: Enter `https://api.hexclave.com/api/v1/auth/oauth/callback/bitbucket` * **Permissions**: Under **Account**, select at minimum **Email** and **Read** 6. Click **Save**. 7. You'll be redirected to the OAuth consumers page. Select your newly created consumer to view its details. 8. Note the **Key** (Client ID) and **Secret** values. Save these somewhere secure as you'll need them for the next steps. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Bitbucket** as the provider. 3. Set the **Client ID** (the Key from your Bitbucket OAuth consumer) and **Client Secret** you obtained from Bitbucket earlier. ### Need More Help? * Check the [Bitbucket OAuth documentation](https://developer.atlassian.com/cloud/bitbucket/oauth-2/) * Join our [Discord](https://discord.hexclave.com) # Custom OIDC Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/custom-oidc Connect any OpenID Connect identity provider to Hexclave Custom OIDC lets you bring **any OpenID Connect-compliant identity provider** - Okta, Auth0, Keycloak, Microsoft Entra ID, Ping, Zitadel, or your own - as a sign-in option, even if it isn't one of Hexclave's built-in providers. Hexclave handles the OAuth flow, OIDC discovery, and account linking; you supply the issuer URL and client credentials. Custom OIDC providers require a **Team plan or above**. Because you're always using your own credentials, there are no shared development keys for custom OIDC. ## How it works You give Hexclave an **issuer URL**, and Hexclave fetches the provider's configuration from its OIDC discovery document (`/.well-known/openid-configuration`) to find the authorization, token, and userinfo endpoints. User profiles are mapped from standard OIDC claims (`sub`, `name` / `preferred_username`, `email`, `email_verified`, `picture`). You can add **multiple** custom OIDC providers, each identified by a unique **provider ID** that you choose. ## Integration Steps ### Choose a provider ID Pick a unique ID for this provider, for example `my-okta`. You'll use it in the callback URL and in your sign-in code. Provider IDs may contain **lowercase letters, numbers, hyphens, and underscores** only, and can't match a built-in provider name (like `google` or `github`). ### Create an OIDC app with your identity provider In your identity provider's admin console, create a new OIDC / OAuth2 web application and set its redirect (callback) URL to: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/YOUR_PROVIDER_ID ``` Replace `YOUR_PROVIDER_ID` with the ID you chose in the previous step. For local development, use `http://localhost:8102/api/v1/auth/oauth/callback/YOUR_PROVIDER_ID`. Then collect: * **Issuer URL** - the base URL of your provider (e.g. `https://your-idp.example.com`). It must support OIDC discovery. * **Client ID** and **Client Secret** from the app you just created. ### Add the provider in Hexclave 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add Custom OIDC**. 3. Fill in the form: * **Provider ID** - the ID you chose (e.g. `my-okta`) * **Display Name** - a human-readable label (e.g. `My Identity Provider`) * **Issuer URL** - your provider's issuer URL * **Client ID** and **Client Secret** - from your provider * **Scopes** (optional) - space-separated OAuth scopes. Defaults to `openid email profile`. 4. Click **Add Provider**. After it's created, choose **Configure** on the provider's row to view the exact **Redirect URL** Hexclave generated, and confirm it matches what you registered with your identity provider. ### Trigger sign-in from your app Custom OIDC providers are not rendered automatically by the prebuilt sign-in buttons, so start the flow yourself with `signInWithOAuth`, passing your provider ID: ```tsx theme={null} "use client"; import { useHexclaveApp } from "@hexclave/next"; // replace `next` with the correct framework SDK package export function OktaSignInButton() { const app = useHexclaveApp(); return ( ); } ``` Custom OIDC providers don't appear in the default `` / `` provider buttons. Add your own button that calls `signInWithOAuth("")` (or build a fully custom sign-in UI). ## Connecting accounts Custom OIDC also works as a [connected account](/guides/apps/authentication/connected-accounts). To link a custom OIDC provider to an already signed-in user, call `linkConnectedAccount` with the same provider ID: ```tsx theme={null} await user.linkConnectedAccount("my-okta"); ``` For non-shared providers like custom OIDC, you can also request additional scopes and retrieve access tokens to call your provider's APIs on the user's behalf. ## Need More Help? * Read about [OpenID Connect discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) * Join our [Discord](https://discord.hexclave.com) # Discord Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/discord Set up Discord as an authentication provider with Hexclave This guide explains how to set up Discord as an authentication provider with Hexclave. Discord OAuth2 allows users to sign in to your application using their Discord account. ## Integration Steps 1. Navigate to the [Discord Developer Portal](https://discord.com/developers/applications). 2. Click the **New Application** button in the top-right corner. 3. Enter a name for your application and click **Create**. You will be redirected to the General Information page. 4. Select **OAuth2** in the left sidebar. 5. Under **Redirects** add `https://api.hexclave.com/api/v1/auth/oauth/callback/discord` 6. In the **OAuth2** section, enable the required scopes: 'identify' and 'email' 7. Click **Save Changes** 8. Save the **Client ID** and **Client Secret**. You may need to select **Reset Secret** to generate a new one. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Discord** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from the Discord Developer Portal earlier. ### User Profile Data When a user signs in with Discord, Hexclave will create a user profile with the following data: * **User ID**: Discord's unique user ID * **Username**: The user's Discord username * **Avatar**: The user's Discord avatar (if available) * **Email**: The user's email if the 'email' scope is requested ### Need More Help? * Check the [Discord OAuth2 Documentation](https://discord.com/developers/docs/topics/oauth2) * Visit our [Discord Support Channel](https://discord.hexclave.com) # Facebook Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/facebook Set up Facebook as an authentication provider with Hexclave This guide explains how to set up Facebook as an authentication provider with Hexclave. Facebook OAuth allows users to sign in to your application using their Facebook account. ## Integration Steps 1. Navigate to the [Facebook Developers Portal](https://developers.facebook.com/). 2. In the top-right, select **My Apps** and then **Create App**. 3. You'll be redirected to the Create an app process. 4. In the **App details** step, select the app type (typically **Consumer** for authentication), fill out the necessary information, and select **Next**. 5. In the **Use Cases** step, select **Authenticate and request data from users with Facebook Login** and then select **Next**. 6. In the **Business** step, select the business portfolio to connect to your app and then select **Next**. 7. In the **Finalize** step, select **Go to dashboard**. You'll be redirected to the app's Dashboard page. 8. In the left sidenav, select **Use cases**. 9. Next to **Authenticate and request data from users with Facebook Login**, select **Customize**. 10. On the Permissions tab, next to **email**, select **Add** to allow Hexclave to read your user's primary email address. 11. In the left sidenav, under **Facebook Login**, select **Settings**. 12. In the **Client OAuth settings** section, in the **Valid OAuth Redirect URIs** field, add `https://api.hexclave.com/api/v1/auth/oauth/callback/facebook` 13. Select **Save changes**. 14. In the left sidenav, select **App settings** (hover over the settings icon), and then select **Basic**. 15. Note your **App ID** and **App Secret** for the next steps. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Facebook** as the provider. 3. Set the **App ID** and **App Secret** you obtained from the Facebook Developers Portal earlier. ### Need More Help? * Check the [Facebook Login Documentation](https://developers.facebook.com/docs/facebook-login/) * Join our [Discord](https://discord.hexclave.com) # GitHub Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/github Set up GitHub as an authentication provider with Hexclave This guide explains how to set up GitHub as an authentication provider with Hexclave. GitHub allows users to sign in to your Hexclave-enabled app using their GitHub account. For Development purposes, Hexclave uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. You should replace these before you go into production. ## Integration Steps If you are unsure if you need to create a GitHub App, or a GitHub OAuth App, check the [Differences On GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps). More than likely, you will want to create a GitHub App. The installation process is the same for both. ### Create a GitHub App 1. Navigate to your [GitHub Developer App Settings](https://github.com/settings/apps). 2. Click the **New GitHub App** button. 3. Enter a name for your application, homepage URL, and a description. 4. For **Authorization callback URL**, add `https://api.hexclave.com/api/v1/auth/oauth/callback/github` 5. For permissions, at a **minimum**, you will need **Account Permissions > Email Addresses** set to **Read Only**. Your sign-in flow will not work without this permission. 6. Select **Any Account** under the ***Where can this GitHub App be installed*** section. 7. Click **Create GitHub App** 8. Save the **Client ID** and click **Generate a new client secret** to create your **Client Secret**. ### Enable GitHub Provider in Hexclave 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **GitHub** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from your GitHub App earlier. *** ### Create an OAuth App 1. Navigate to your [GitHub Developer Settings](https://github.com/settings/developers). 2. Click the **New OAuth App** button. 3. Enter a name for your application, homepage URL, and a description. 4. For **Authorization callback URL**, add `https://api.hexclave.com/api/v1/auth/oauth/callback/github` 5. Click **Register application** 6. Save the **Client ID** and click **Generate a new client secret** to create your **Client Secret**. ### Enable GitHub OAuth in Hexclave 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **GitHub** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from GitHub earlier. ### Need More Help? * Check the [GitHub OAuth Documentation](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps) * Join our [Discord](https://discord.hexclave.com) # GitLab Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/gitlab Set up GitLab as an authentication provider with Hexclave This guide explains how to set up GitLab as an authentication provider with Hexclave. GitLab OAuth allows users to sign in to your application using their GitLab account. ## Integration Steps 1. Log in to your GitLab account. 2. In the top-right corner, click on your profile picture and select **Preferences**. 3. In the left sidebar, select **Applications** > **Add new application**. 4. Fill out the form with the following details: * **Name**: Choose a name for your application * **Redirect URI**: Enter `https://api.hexclave.com/api/v1/auth/oauth/callback/gitlab` * **Scopes**: Select at minimum the `profile` and `email` scopes 5. Click **Save application**. 6. GitLab will display your **Application ID** and **Secret**. Make note of these values as you'll need them for the next steps. 7. If you're using a self-hosted GitLab instance, you'll also need the URL of your GitLab instance. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **GitLab** as the provider. 3. Set the **Application ID** and **Secret** you obtained from GitLab earlier. 4. If you're using a self-hosted GitLab instance, you'll also need to provide the URL for your instance. For gitlab.com, you can leave this field blank or use the default value. ### Need More Help? * Check the [GitLab OAuth 2.0 documentation](https://docs.gitlab.com/ee/api/oauth2.html) * Join our [Discord](https://discord.hexclave.com) # Google Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/google Set up Google as an authentication provider with Hexclave This guide explains how to set up Google as an authentication provider with Hexclave. Google OAuth2 allows users to sign in to your application using their Google account. For Development purposes, Hexclave uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. You should replace these before you go into production. ## Integration Steps ### Create a Google OAuth2 App 1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/). 2. Create a new project or select an existing one. 3. In the sidebar, navigate to **APIs & Services** > **Credentials**. 4. Click **Create Credentials** and select **OAuth client ID**. 5. Select **Web application** as the application type. 6. Enter a name for your OAuth client. 7. Under **Authorized redirect URIs**, add `https://api.hexclave.com/api/v1/auth/oauth/callback/google` 8. Click **Create**. 9. Save the **Client ID** and **Client Secret** that are displayed. ### Enable Google OAuth2 in Hexclave 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Google** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from Google Cloud Console earlier. ### Need More Help? * Check the [Google OAuth2 Documentation](https://developers.google.com/identity/protocols/oauth2) * Join our [Discord](https://discord.hexclave.com) # LinkedIn Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/linkedin Set up LinkedIn as an authentication provider with Hexclave This guide explains how to set up LinkedIn as an authentication provider with Hexclave. LinkedIn OAuth2 allows users to sign in to your application using their LinkedIn account. ## Integration Steps 1. Log in to the [LinkedIn Developer Portal](https://www.linkedin.com/developers/apps). 2. Click **Create app** to create a new application. 3. Enter your **App name** and select a **LinkedIn Page** to associate with your app (or create a new one). 4. Upload an **App logo** (required for production apps). 5. Enter the **App description** and your **Business email**. 6. Check the **Legal agreement** box and click **Create app**. 7. On your app's dashboard, click **Auth** tab from the left sidebar. 8. Under **OAuth 2.0 settings**, add the following redirect URL: `https://api.hexclave.com/api/v1/auth/oauth/callback/linkedin` 9. Under **Products**, request access to **Sign In with LinkedIn** by clicking **Request access**. Complete any required information. 10. Under **OAuth 2.0 scopes**, make sure at least the following scopes are selected: * `r_emailaddress` * `r_liteprofile` 11. Once approved, navigate to the **Auth** tab again to find your **Client ID** and **Client Secret**. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **LinkedIn** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from the LinkedIn Developer Portal earlier. ### Need More Help? * Check the [LinkedIn OAuth2 Documentation](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow) * Join our [Discord](https://discord.hexclave.com) # Microsoft Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/microsoft Set up Microsoft as an authentication provider with Hexclave This guide explains how to set up Microsoft as an authentication provider with Hexclave. Microsoft OAuth allows users to sign in to your application using their Microsoft account. For Development purposes, Hexclave uses shared keys for this provider. Shared keys are automatically created by Hexclave, but show Hexclave's logo on the OAuth sign-in page. You should replace these before you go into production. ## Integration Steps 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) (formerly Azure AD). 2. In the left sidebar, go to **Applications** > **App registrations**. 3. Click **New registration** at the top of the page. 4. Enter a name for your application. 5. Under **Supported account types**, select the option that best suits your needs (typically **Accounts in any organizational directory and personal Microsoft accounts**). 6. In the **Redirect URI** section, select **Web** as the platform and enter `https://api.hexclave.com/api/v1/auth/oauth/callback/microsoft` 7. Click **Register** to create the application. 8. You'll be redirected to the app's Overview page. Note the **Application (client) ID** displayed at the top. 9. In the left sidebar, click **Certificates & secrets**. 10. Under **Client secrets**, click **New client secret**. 11. Add a description, select an expiration period, and click **Add**. 12. Copy the **Value** of the client secret immediately (you won't be able to see it again). 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Microsoft** as the provider. 3. Set the **Client ID** (Application ID) and **Client Secret** you obtained from the Microsoft Entra admin center. ## Things to Know About Microsoft OAuth * **Emails are not marked as verified.** Microsoft doesn't attest that the user controls the email it returns, so Hexclave treats Microsoft emails as unverified. See Microsoft's [claims validation guidance](https://learn.microsoft.com/en-us/entra/identity-platform/claims-validation#validate-the-subject). * **Supported account types control who can sign in** (custom OAuth keys only). When using your own Microsoft OAuth app, you can set the tenant type in the Hexclave dashboard or config. The value maps to the `{tenant}` segment of Microsoft's authorize/token endpoints: `common` (work/school **and** personal accounts), `organizations` (work/school only), `consumers` (personal only, the default), or a specific tenant ID/domain. See [Microsoft's endpoint reference](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints). This setting does not apply to the shared development keys. ### Need More Help? * Check the [Microsoft identity platform Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/) * Join our [Discord](https://discord.hexclave.com) # Passkey Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/passkey Set up passkey authentication with Hexclave using WebAuthn This guide explains how to set up Passkey authentication with Hexclave. Passkeys allow users to sign in to your application securely using biometrics, mobile devices, or security keys. Passkeys provide a more secure and convenient authentication method compared to traditional passwords by using WebAuthn standard. ## Integration Steps 1. Log in to the [Hexclave dashboard](https://app.hexclave.com/). 2. Select your project from the dashboard. 3. In the left sidebar, select **Auth Methods**. 4. Find the **Passkey** authentication method and toggle it to enable. 5. Save your changes. 1. Make sure you've installed the right Hexclave SDK package for your framework. For example, in Next.js: ```bash theme={null} npm install @hexclave/next ``` For other frameworks, use the package shown in [Setup](/guides/getting-started/setup), such as `@hexclave/react`, `@hexclave/js`, or `@hexclave/tanstack-start`. 2. Add Passkey support to your sign-in component by using the built-in Hexclave components or creating your own implementation with the SDK. Using built-in components: ```jsx theme={null} import { SignIn } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function SignInPage() { return ; } ``` The built-in components will automatically show the passkey option when it's enabled in your project. ## How Passkey Authentication Works 1. **Registration**: When a user creates a new passkey, their device generates a unique public-private key pair. The private key stays on the user's device, while the public key is sent to Hexclave's servers. 2. **Authentication**: When a user wants to sign in, Hexclave sends a challenge to the user's device. The device uses the private key to sign the challenge, and sends the signature back to Hexclave for verification. 3. **Cross-device authentication**: Users can create passkeys on one device and use them to sign in on another device using QR codes or nearby device detection. For the most up-to-date compatibility information, refer to the [WebAuthn browser compatibility chart](https://caniuse.com/webauthn). # Spotify Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/spotify Set up Spotify as an authentication provider with Hexclave This guide explains how to set up Spotify as an authentication provider with Hexclave. Spotify OAuth allows users to sign in to your application using their Spotify account. For Development purposes, Hexclave uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. You should replace these before you go into production. ## Integration Steps 1. Navigate to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/). 2. Log in with your Spotify account. 3. Click **Create app** to create a new application. 4. Enter an **App name** and **App description**. 5. Under **Redirect URI**, add `https://api.hexclave.com/api/v1/auth/oauth/callback/spotify` 6. Check the agreement checkbox and click **Create**. 7. You'll be redirected to your app's dashboard. Note your **Client ID** displayed on this page. 8. Click **Settings** to view more details about your app. 9. In the settings page, you can view your **Client Secret** by clicking **Show client secret**. 10. If needed, you can adjust the app settings, including adding additional redirect URIs. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Spotify** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from the Spotify Developer Dashboard earlier. ### Need More Help? * Check the [Spotify Web API Authorization Documentation](https://developer.spotify.com/documentation/general/guides/authorization/) * Join our [Discord](https://discord.hexclave.com) # Twitch Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/twitch Set up Twitch as an authentication provider with Hexclave This guide explains how to set up Twitch as an authentication provider with Hexclave. Twitch OAuth allows users to sign in to your application using their Twitch account. ## Integration Steps 1. Navigate to the [Twitch Developer Console](https://dev.twitch.tv/console). 2. Log in with your Twitch account. 3. Navigate to **Applications** and click **Register New Application**. 4. Enter a **Name** and select a **Category**. 5. Under **OAuth Redirect URLs**, add `https://api.hexclave.com/api/v1/auth/oauth/callback/twitch` 6. Click **Create**. 7. You'll be redirected to your app's dashboard. 8. Click **Manage** of the app you just created to view more details about your app. 9. Click "New Secret" to generate a new secret. 10. Copy and save the **Client ID** and **Client Secret**. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **Twitch** as the provider. 3. Set the **Client ID** and **Client Secret** you obtained from the Twitch Developer Console earlier. ### Need More Help? * Check the [Twitch OAuth Documentation](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/) * Join our [Discord](https://discord.hexclave.com) # Two-Factor Authentication (2FA) Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/two-factor-auth Learn how Two-Factor Authentication works with Hexclave This guide explains how Two-Factor Authentication (2FA) works with Hexclave. 2FA adds an extra layer of security by requiring users to provide a verification code in addition to their password. Hexclave implements TOTP (Time-based One-Time Password) for two-factor authentication, which is compatible with standard authenticator apps like Google Authenticator, Microsoft Authenticator, and Authy. 2FA is enabled by default at the platform level and can be configured by individual users. ## Integration Steps 2FA is enabled by default on the Hexclave platform. Unlike other authentication methods, you don't need to enable it specifically for your project. To allow your users to set up 2FA for their accounts: 1. Make sure you've installed the right Hexclave SDK package for your framework. For example, in Next.js: ```bash theme={null} npm install @hexclave/next ``` For other frameworks, use the package shown in [Setup](/guides/getting-started/setup), such as `@hexclave/react`, `@hexclave/js`, or `@hexclave/tanstack-start`. 2. Use the Hexclave components to give users access to their account settings, where they can enable 2FA: ```jsx theme={null} import { AccountSettings } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function SettingsPage() { return ; } ``` 3. The built-in Hexclave components will handle the entire 2FA setup process, including QR code generation, verification, and recovery codes. ## How Hexclave 2FA Works Hexclave uses the industry-standard TOTP (Time-based One-Time Password) algorithm for two-factor authentication: 1. **User Setup**: When a user enables 2FA in their account settings, Hexclave generates a secret key that is shared with the user's authenticator app (usually via a QR code). 2. **Code Generation**: The authenticator app generates a 6-digit code that changes every 30 seconds, based on the shared secret and the current time. ## Recommended Authenticator Apps The following authenticator apps are compatible with Hexclave 2FA: * Google Authenticator (Android, iOS) * Microsoft Authenticator (Android, iOS) * Authy (Android, iOS, desktop) * 1Password (Android, iOS, desktop) * LastPass Authenticator (Android, iOS) # X (Twitter) Source: https://docs.hexclave.com/guides/apps/authentication/auth-providers/x-twitter Set up X (Twitter) as an authentication provider with Hexclave This guide explains how to set up X (formerly Twitter) as an authentication provider with Hexclave. X OAuth 2.0 allows users to sign in to your application using their X account. ## Integration Steps 1. Log in to the [X Developer Portal](https://developer.twitter.com/). 2. Navigate to the [Developer Portal Dashboard](https://developer.twitter.com/en/portal/dashboard). 3. Click on **+ Create Project** to create a new project. 4. Enter a name for your project and select **Web App, Automated App or Bot** as the use case, then click **Next**. 5. Enter a description for your project and click **Next**. 6. Name your app and click **Next**. 7. In the **App settings** section, find your API Key and Secret. These will serve as your OAuth 2.0 Client ID and Client Secret. 8. In the left sidebar, click on your project, then select the app you just created. 9. Click on the **Settings** tab and scroll to the **User authentication settings**. 10. Click **Set up** or **Edit** if already configured. 11. Enable **OAuth 2.0** and set the following details: * **Type of App**: Web App * **Callback URL / Redirect URL**: `https://api.hexclave.com/api/v1/auth/oauth/callback/x` * **Website URL**: Your website's URL 12. Under **App permissions**, select your scopes. 13. Click **Save** to apply your changes. 1. On the Hexclave dashboard, select **Auth Methods** in the left sidebar. 2. Click **Add SSO Providers** and select **X (Twitter)** as the provider. 3. Set the **Client ID** (your API Key) and **Client Secret** you obtained from the X Developer Portal earlier. ### Need More Help? * Check the [X OAuth 2.0 documentation](https://developer.twitter.com/en/docs/authentication/oauth-2-0) * Join our [Discord](https://discord.hexclave.com) # CLI App Authentication Source: https://docs.hexclave.com/guides/apps/authentication/cli-authentication How to authenticate users in your own command-line application using Hexclave If you're building your own command-line application, you can use Hexclave to let users log in from a terminal. This page is about adding authentication to your own CLI app. For the official Hexclave CLI, see the [Hexclave CLI guide](/guides/going-further/cli). To do so, we provide a Python template that you can use as a starting point. [Download it here](https://github.com/hexclave/hexclave/tree/main/docs/public/hexclave-cli-template.py) and copy it into your project, for example: ``` └─ my-python-app ├─ main.py └─ hexclave_cli_template.py # <- the file you just downloaded (rename to use underscores for Python import) ``` Then, you can import the `prompt_cli_login` function. The project ID is enough for most projects; only pass `publishable_client_key` if the project has `requirePublishableClientKey` enabled. ```py theme={null} from hexclave_cli_template import prompt_cli_login # prompt the user to log in refresh_token = prompt_cli_login( app_url="https://your-app-url.example.com", project_id="your-project-id-here", ) if refresh_token is None: print("User cancelled the login process. Exiting") exit(1) # you can also store the refresh token in a file, and only prompt the user to log in if the file doesn't exist # you can now use the REST API with the refresh token def hexclave_request(method, endpoint, **kwargs): # ... see the REST API overview for required Hexclave headers # https://docs.hexclave.com/api/overview def get_access_token(refresh_token): access_token_response = hexclave_request( 'post', '/api/v1/auth/sessions/current/refresh', headers={ 'x-hexclave-refresh-token': refresh_token, } ) return access_token_response['access_token'] def get_user_object(access_token): return hexclave_request( 'get', '/api/v1/users/me', headers={ 'x-hexclave-access-token': access_token, } ) user = get_user_object(get_access_token(refresh_token)) print("The user is logged in as", user['display_name'] or user['primary_email']) ``` # Connected Accounts Source: https://docs.hexclave.com/guides/apps/authentication/connected-accounts Managing third-party OAuth access tokens Stack has good support for working with OAuth and OIDC providers, such as Google, Facebook, Microsoft, and others. Beyond using OAuth for signing in, Stack can manage your users' access token so you can invoke APIs on their behalf. For example, you can use this to send emails with Gmail, access repositories on GitHub, or access files on OneDrive. A connected account is simply an external account that is linked to the user in some way. If you are not using shared keys (see note below), any user created with "Sign up with OAuth" is automatically connected to the account they signed up with, but it's also possible to connect a user with a provider that is unavailable for sign in. You cannot connect a user's accounts with shared OAuth keys. You need to set up your own OAuth client ID and client secret in Stack's dashboard. For more details, check [Going to Production](/guides/apps/launch-checklist/overview#oauth-providers). ## Connecting with OAuth providers You can access a user's connected account with the `user.getConnectedAccount(providerId)` function or `user.useConnectedAccount(providerId)` hook. Often, you'll want to redirect the user to the OAuth provider's authorization page if they have not connected the account yet. Just like the `getUser(...)` function, `getConnectedAccount(...)` can also take an `{ or: "redirect" }` argument to achieve this. Here's how to connect with Google: ```jsx theme={null} 'use client'; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function Page() { const user = useUser({ or: 'redirect' }); // Redirects to Google authorization if not already connected const account = user.useConnectedAccount('google', { or: 'redirect' }); // Account is always defined because of the redirect return
Google account connected
; } ``` ## Providing scopes Most providers have access control in the form of OAuth scopes. These are the permissions that the user will see on the authorization screen (eg. "Your App wants access to your calendar"). For instance, to read Google Drive content, you need the `https://www.googleapis.com/auth/drive.readonly` scope: ```jsx theme={null} 'use client'; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function Page() { const user = useUser({ or: 'redirect' }); // Redirects to the Google authorization page, requesting access to Google Drive const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/auth/drive.readonly'] }); // Account is always defined because of the redirect return
Google Drive connected
; } ``` Check your provider's API documentation to find a list of available scopes. ## Retrieving the access token Once connected with an OAuth provider, obtain the access token with the `account.getAccessToken()` function. Check your provider's API documentation to understand how you can use this token to authorize the user in requests. ```jsx theme={null} 'use client'; import { useEffect, useState } from 'react'; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function Page() { const user = useUser({ or: 'redirect' }); const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/auth/drive.readonly'] }); const { accessToken } = account.useAccessToken(); const [response, setResponse] = useState(); useEffect(() => { fetch('https://www.googleapis.com/drive/v3/files', { headers: { Authorization: `Bearer ${accessToken}` } }) .then((res) => res.json()) .then((data) => setResponse(data)) .catch((err) => console.error(err)); }, [accessToken]); return
{response ? JSON.stringify(response) : 'Loading...'}
; } ``` ## Sign-in default scopes To avoid showing the authorization page twice, you can already request scopes during the sign-in flow. This approach is optional. Some applications may prefer to request extra permissions only when needed, while others might want to obtain all necessary permissions upfront. To do this, edit the `oauthScopesOnSignIn` setting of your `hexclaveServerApp`: ```jsx title="stack/server.ts" theme={null} export const hexclaveServerApp = new HexclaveServerApp({ // ...your other settings... oauthScopesOnSignIn: { google: ['https://www.googleapis.com/auth/drive.readonly'] } }); ``` ## OAuth account merging strategies When a user attempts to sign in with an OAuth provider that matches an existing account, Stack provides different strategies for handling the authentication flow. The available strategies are: * Allow duplicates (legacy default) * Link method (new default) * Block duplicates (most secure) The "Link" strategy is the default behavior. If a user attempts to sign in with an OAuth provider that matches an existing account, Stack will link the OAuth identity to the existing account, and the user will be signed into that account. This requires both of the credentials to be verified, or otherwise the link will be blocked, in the same way as the "Block" strategy. The "Allow" strategy is the default behavior for old projects. If a user attempts to sign in with an OAuth provider that has an existing account with the same email address, Stack will create a separate account for the user. The "Block" strategy will explicitly raise an error if a user attempts to sign in with an OAuth provider that matches an existing account. # Fraud Protection Source: https://docs.hexclave.com/guides/apps/authentication/fraud-protection Detect bots, free-trial abuse, and other fraudulent sign-ups. Fraud Protection is a sub-app of [Authentication](./overview). It isn't a separate page or a separate toggle - it's the name we give to the **risk signals** that the Authentication app already attaches to every sign-up attempt (bot score, free-trial abuse score, and country), and the conditions you can write against them in [Sign-up Rules](./sign-up-rules). In the dashboard, Fraud Protection appears as its own tile (grouped under the Authentication category) with a **Go to Authentication** button that takes you straight to **Authentication → Sign-up Rules**. There's nothing to configure on a separate screen - everything happens in the rule builder. Since Authentication is always on, Fraud Protection is on. There is no independent toggle. ## Available signals Every sign-up attempt is scored with the following fields. They're always available in the CEL condition builder alongside `email`, `emailDomain`, `authMethod`, and `oauthProvider`. The **Operators** column lists the choices shown in the builder's dropdown; under the hood each rule is stored as a CEL expression, so `equals` becomes `==`, `in_list` becomes `in [...]`, `greater_or_equal` becomes `>=`, and so on: | Field | Type | Operators | Description | | ----------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `countryCode` | string (ISO-3166-1 alpha-2) | `equals`, `not_equals`, `in_list` | Country associated with the sign-up attempt (e.g. `"US"`, `"DE"`, `"NG"`). Empty if unavailable. | | `riskScores.bot` | number (0-100) | `equals`, `not_equals`, `greater_than`, `greater_or_equal`, `less_than`, `less_or_equal` | Bot risk score. Higher means more likely automated. | | `riskScores.free_trial_abuse` | number (0-100) | same as `riskScores.bot` | Free-trial abuse risk score. Higher means more likely abuse. | These fields evaluate together with the rest of the sign-up context, so a single rule can combine them freely. ## Using signals in rules Open **Authentication → Sign-up Rules → Add rule**. The rule builder is the same one documented in [Sign-up Rules](./sign-up-rules#creating-rules) - the fraud-specific fields just appear in the field dropdown. ### Block obvious bots * Condition: `riskScores.bot >= 80` * Action: **Reject** ### Restrict suspected free-trial abuse for manual review Send borderline accounts to a restricted state instead of blocking outright, so support can review them: * Condition: `riskScores.free_trial_abuse >= 60` * Action: **Restrict** **What Restrict means:** the account is created, but marked restricted (`isRestricted`). Hexclave treats restricted users like unauthenticated users in most SDK calls until an admin clears the restriction or they finish whatever verification is required. See [Restricted Users](./restricted-users) for SDK handling and JWT claims, and [Sign-up Rules → Restrict](./sign-up-rules#restrict) for how the action behaves in the rule engine. ### Combine signals with email and geo Allow sign-ups from a known corporate domain, but still hard-block anything that smells like automation: 1. Rule 1: `emailDomain == "company.com" && riskScores.bot < 70` → **Allow** 2. Rule 2: `riskScores.bot >= 70` → **Reject** 3. Rule 3: `countryCode in ["CN", "RU"] && riskScores.free_trial_abuse >= 40` → **Restrict** 4. Default: **Allow** Remember rules are evaluated **top-to-bottom by priority**. A matching **Allow** or **Reject** is terminal - it stops evaluation and decides the outcome. A matching **Restrict** or **Log** is recorded but evaluation *continues* down the list (only the first matching `Restrict` rule is attributed to the user). Place explicit allows above broader blocks if you want allow-lists to short-circuit. ### Log first, enforce later When you start tuning thresholds, set the action to **Log** instead of `Reject` / `Restrict`. The rule will trigger and show up in the per-rule sparkline + trigger history, but the sign-up flow is unaffected. Once you're confident, switch the action to `Reject` or `Restrict`. ## Testing fraud signals The Sign-up Rules **rule tester** (button **Open tester** at the bottom of the page) has a dedicated **Risk overrides** section for the fraud fields: * **Country** - override the resolved country code (any ISO-3166-1 alpha-2). * **Bot score** - 0–100. Must be provided together with **Free-trial abuse** or both must be blank. * **Free-trial abuse** - 0–100. Same pairing rule as Bot score. Click **Run test** to see how each rule evaluates against the simulated context. The result panel shows: * **Outcome** - allow / reject and whether it came from a rule or the default action. * **Triggered rules** - which rules matched, plus which one was the decision. * **Evaluation trace** - every rule's status (`Matched` / `No match` / `Disabled` / `No condition` / `Error`). * **Normalized context** - the exact values the engine used, so you can sanity-check your overrides against the rendered context. This is the safest way to validate a new threshold before flipping it from **Log** to **Reject**. ## Trigger history & analytics Each rule row on the Sign-up Rules page shows a **sparkline** with the count of triggers in the recent analytics window (typically last 48h). Click the sparkline to open the **trigger history** dialog, which shows: * All-time and recent counts. * A per-rule activity chart. * A paginated list of every individual trigger (timestamp + email when captured). For high-risk rules, this is where you'll watch volume in real time as you tune `riskScores.*` thresholds. ## On the user page The fraud signals also surface per-user. Open any user from **Users → \**, switch to the **Authentication** tab, and scroll to the **Fraud** section. It shows a 2-column grid with four fields: | Field | Editable | Notes | | -------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Manual restriction** | via dialog | Status text - `Not restricted` / `Restricted by admin` / `Not manually restricted ()`. The reason mirrors the underlying restriction (`anonymous`, `email_not_verified`, `restricted_by_administrator`). | | **Risk score: bot** | inline | `user.riskScores.signUp.bot` (0-100). Click to override. | | **Risk score: free trial abuse** | inline | `user.riskScores.signUp.freeTrialAbuse` (0-100). Click to override. | | **Sign-up country code** | inline | `user.countryCode` (ISO-3166-1 alpha-2, uppercased). Empty if not resolved. | These are the same values the [Sign-up Rules engine](./sign-up-rules) saw at sign-up time. Editing them is useful for back-filling test data, fixing a miscalibrated score, or overriding the country before re-running a downstream flow. ### Restricting a user A **Restrict user** button sits in the section header (and the action also lives in the user's top-right `⋮` menu). It's red (destructive) while the user has no manual restriction, and switches to an outline style once one is in place. It opens the **User Restriction** dialog with two fields: * **Public reason** - shown to the user when they try to access your app. * **Private details** - admin-only notes (e.g. internal ticket links). Click **Save & restrict user** to mark the user as `restrictedByAdmin: true`. The action label reflects the user's current state: **Restrict user** when they're unrestricted, **Add manual restriction** when they're already restricted for another reason (e.g. unverified email), and **Edit or remove manual restriction** once a manual restriction exists - in which case the dialog also gains a **Remove manual restriction** action. A `Restrict` outcome from your sign-up rules also lands users here - but with `restrictedReason.type === "restricted_by_administrator"` you can tell the difference between a rule-driven restriction (which carries a rule ID in analytics) and a hand-picked manual one. ### Restriction banner If a user is restricted for any reason, a destructive banner shows at the top of their page explaining why: * `anonymous` - Anonymous users must sign up with credentials to remove this restriction. * `email_not_verified` - The user needs to verify their email address. * `restricted_by_administrator` - Shows the public reason and private details if set. The banner also exposes the same restriction action button so you can manage it without scrolling. ## Related * [Authentication Overview](./overview) - parent app. * [Sign-up Rules](./sign-up-rules) - the enforcement layer that consumes these signals. * [Restricted Users](./restricted-users) - what restricted accounts can and can't do. * [JWT Tokens](./jwts) - how the `Restrict` action surfaces in user tokens. # Authentication Source: https://docs.hexclave.com/guides/apps/authentication/guide Implement Hexclave authentication from start to finish — sign-in methods, auth pages, session reads, and route protection. This guide walks through implementing authentication in your app end to end: pick your sign-in methods, decide where auth pages live, wire up sign-in and sign-out, read the current user, then protect the parts of your app that need a session. For a quick "can I do this?" checklist, see the [Authentication overview](./overview). You'll need Hexclave installed with a client app (`hexclaveClientApp`), plus a server app (`hexclaveServerApp`) if you read the user on the server. If you don't have that yet, follow [Setup](/guides/getting-started/setup) first. ## 1. Choose your sign-in methods New projects come with the **Authentication** app already enabled and **email & password** sign-in already turned on, so you have a working way in before you configure anything. Everything else — OTP, passkeys, and every OAuth provider — starts off, so step 1 is really about changing the mix. In a [development environment](/guides/going-further/local-vs-cloud-dashboard), set the mix in `hexclave.config.ts` so it's versioned with your code: ```ts title="hexclave.config.ts" theme={null} import type { HexclaveConfig } from "@hexclave/js"; export const config: HexclaveConfig = { auth: { allowSignUp: true, otp: { allowSignIn: true }, password: { allowSignIn: false }, }, "auth.oauth": { accountMergeStrategy: "link_method", providers: { google: { type: "google", allowSignIn: true, allowConnectedAccounts: true }, }, }, }; ``` That example swaps the default password login for OTP plus one OAuth provider — a reasonable SaaS default, since there are no passwords to reset and one familiar button. Keep `password: { allowSignIn: true }` instead if you want classic email and password, and add `passkey: { allowSignIn: true }` for WebAuthn. | Method | Config | Default | Notes | | --------------------- | --------------------------- | ------- | ----------------------------------------------------------------------------------- | | **Email & password** | `auth.password.allowSignIn` | **On** | Includes the reset flow. | | **OTP / magic link** | `auth.otp.allowSignIn` | Off | Passwordless. Uses your email server — Hexclave's shared server covers development. | | **Passkey** | `auth.passkey.allowSignIn` | Off | See [Passkey](./auth-providers/passkey). | | **OAuth** | `auth.oauth.providers.` | Off | 12 providers, plus your own [OIDC provider](./auth-providers/custom-oidc). | | **Two-factor (TOTP)** | Dashboard | Off | See [Two-Factor Auth](./auth-providers/two-factor-auth). | `auth.allowSignUp` is also on by default, so anyone can create an account until you say otherwise — step 8 covers narrowing that. If the Authentication app was ever turned off for this project, re-enable it with `apps: { installed: { authentication: { enabled: true } } }` or the dashboard's app list. Google, GitHub, Microsoft, and Spotify work immediately on Hexclave's shared OAuth keys, so you can enable them without registering an app anywhere. Client IDs and secrets are environment-specific and live in the [cloud dashboard](https://app.hexclave.com), not in `hexclave.config.ts` — you'll swap in your own before production in step 9. `accountMergeStrategy: "link_method"` means someone who signed up with a password and later clicks "Sign in with Google" on the same email lands on their existing account rather than a duplicate. See [Connected accounts](./connected-accounts) for the other strategies. ## 2. Decide where your auth pages live Hexclave renders sign-in, sign-up, password reset, and account settings for you. You only choose *where* those pages are served, via the `urls` option on your app object. For new projects, use hosted components: ```ts title="src/hexclave/client.ts" theme={null} import { HexclaveClientApp } from "@hexclave/next"; // replace `next` with the correct framework SDK package export const hexclaveClientApp = new HexclaveClientApp({ tokenStore: "nextjs-cookie", // "cookie" for other web frontends urls: { default: { type: "hosted", }, }, }); ``` Hexclave serves the pages, they stay up to date on their own, and you don't add any routes to your app. The alternative is `{ type: "handler-component" }` plus a catch-all route on your own domain, which you want when auth UI has to be same-origin. [Hosted vs. Handler](/guides/going-further/hosted-vs-handler) covers the tradeoff and how to mix the two. ## 3. Add sign-in, sign-out, and account entry points With the pages in place, your app needs to send people to them. The quickest route is ``, which covers both states on its own — an avatar menu with account settings and sign-out when someone is signed in, and sign-in and sign-up items when they aren't. It brings its own Suspense boundary, so you can drop it straight into a header: ```tsx theme={null} import { UserButton } from "@hexclave/next"; // replace `next` with the correct framework SDK package export function Header() { return
; } ``` Every piece of it is also available on its own, for when you want your own markup. Use these helpers rather than building URLs by hand: ```tsx theme={null} "use client"; import { useUser, useHexclaveApp } from "@hexclave/next"; // replace `next` with the correct framework SDK package export function AuthButtons() { const app = useHexclaveApp(); const user = useUser(); if (user == null) { return ; } return ; } ``` `app.redirectToSignUp()` and `app.redirectToAccountSettings()` round out the set. `user.signOut()` clears the session and then follows your `afterSignOut` or `home` URL. Reading `app.urls.signIn` **throws** when the app is configured for hosted components, because there is no path on your domain to link to. Call `app.redirectToSignIn()` instead of putting `app.urls.signIn` in an `href`. Prefer auth pages inside your own layout? Mount the prebuilt ``, ``, or `` components on a route you own, then point the matching `urls` key at that route so the redirect helpers agree with reality: ```ts theme={null} urls: { default: { type: "hosted" }, accountSettings: { type: "custom", url: "/settings", version: 0 }, }, ``` A bare path string (`accountSettings: "/settings"`) still works but is deprecated, so prefer the `{ type: "custom", ... }` form for new code. ## 4. Read the current user The current user is available on the client as a hook and on the server as an async call, fully typed in both places. Both return `null` when nobody is signed in. The server version is a superset: it can read and write `serverMetadata` and other privileged fields, but it drops session-only methods like `signOut()`, which only make sense where there's a browser session. ```tsx theme={null} // Client component — re-renders whenever the user changes "use client"; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export function Greeting() { const user = useUser(); if (user == null) return

Not signed in

; return

Hi, {user.displayName ?? user.primaryEmail}

; } ``` ```typescript theme={null} // Server component, route handler, or server action import { hexclaveServerApp } from "@/hexclave/server"; const user = await hexclaveServerApp.getUser(); ``` `useUser()` suspends while it loads, so it needs a Suspense boundary above it — [Setup](/guides/getting-started/setup) has a dedicated step for adding one. Store your own fields on the user with `clientMetadata`, `clientReadOnlyMetadata`, and `serverMetadata` instead of standing up a separate users table; see [User fundamentals](/guides/getting-started/user-fundamentals). ## 5. Protect a page or route Pass `or` to turn "maybe a user" into "definitely a user". The return type becomes non-nullable, so there's no `null` branch to forget: ```tsx theme={null} // Sends unauthenticated visitors to sign-in, then back here afterwards const user = useUser({ or: "redirect" }); ``` ```typescript theme={null} // API routes and server actions, where a redirect makes no sense const user = await hexclaveServerApp.getUser({ or: "throw" }); ``` Protect on the server for anything that must not leak. A client-side redirect hides UI but the request already happened, so gate the data too — [Ship production-ready auth](/guides/other/tutorials/ship-production-ready-auth) walks through what each option actually guarantees, including why you should send `Cache-Control: private, no-store` on authenticated responses. ## 6. Collect extra information on sign-up (optional) If you need a name, company, or address before the app is usable, don't redirect to an onboarding page straight after sign-up — users close that tab, and it fights with the "return to the page I originally wanted" redirect. Store a flag on the user instead and check it where onboarding matters: ```tsx theme={null} await user.update({ clientMetadata: { onboarded: true, address }, }); ``` Write the flag to `clientReadOnlyMetadata` from a server endpoint if onboarding must not be skippable, since clients can write their own `clientMetadata`. Full implementation, including the redirect hook: [Onboarding](./user-onboarding). ## 7. Verify sessions on a separate backend If your API is a different service, the browser sends the user's access token and your backend verifies it. Hexclave issues standard JWTs you can verify locally against a JWKS endpoint, so there's no round-trip to Hexclave per request — fast enough for middleware and edge functions. Send the token from the frontend: ```typescript theme={null} const authorizationHeader = await hexclaveClientApp.getAuthorizationHeader(); const response = await fetch("/my-backend-endpoint", { headers: { ...(authorizationHeader != null ? { Authorization: authorizationHeader } : {}), }, }); ``` In a JS/TS backend, hand the request straight to the server app: ```typescript theme={null} const user = await hexclaveServerApp.getUser({ tokenStore: request }); ``` For other languages, verify the JWT yourself against `https://api.hexclave.com/api/v1/projects//.well-known/jwks.json`. Note that anonymous and restricted users are signed with different issuers and audiences, so decide deliberately whether to accept them. See [JWTs & session verification](./jwts) and [Restricted users](./restricted-users). ## 8. Control who can sign up (optional) By default anyone can create an account. [Sign-up rules](./sign-up-rules) are ordered checks over `email`, `emailDomain`, `authMethod`, and `oauthProvider` that fire during sign-up for every method. The first matching rule wins; if none match, the default action applies. | Action | Effect | | ------------ | --------------------------------------------------------------------------- | | **Allow** | Signs up normally. Use to carve exceptions out of a `reject` default. | | **Reject** | Blocks the sign-up. | | **Restrict** | Creates the account in a [restricted](./restricted-users) state for review. | | **Log** | Records a match without changing the outcome. | Start with `log` rules to see what they'd catch, then promote them to `reject` or `restrict`. The dashboard has a tester that simulates sign-ups without touching real users, and [Fraud protection](./fraud-protection) adds risk signals you can reference in the same conditions. ## 9. Before you ship to production The defaults that make development frictionless are exactly the ones you need to replace. The [Launch checklist](../launch-checklist/overview) tracks these four in this order: 1. **Domains** — add your production domain as a trusted domain. 2. **OAuth providers** — swap Hexclave's shared keys for your own client ID and secret per provider, and register the matching redirect URLs. Shared keys are development-only. 3. **Email server** — connect a custom server so verification, reset, and magic link mail comes from your domain. See the [Emails guide](../emails/guide). 4. **Production mode** — turn it on once the first three are done. OAuth client IDs and secrets, trusted domains, and email credentials are all environment-specific, so they live in the [cloud dashboard](https://app.hexclave.com) rather than `hexclave.config.ts`. ## What you should have now 1. The sign-in methods you want, enabled in config 2. Auth pages served either by Hexclave or from your own handler route 3. Sign-in, sign-out, and account settings reachable from your UI 4. `useUser()` / `getUser()` reading the session on client and server 5. At least one route that unauthenticated visitors cannot reach 6. A plan for verifying sessions on any separate backend Everything else — [teams](../teams/overview), [RBAC](../rbac/overview), [payments](../payments/overview), [emails](../emails/overview), [analytics](../analytics/overview) — keys off this same user directory. ## Related * [Authentication overview](./overview) — capability FAQ * [Hosted vs. Handler](/guides/going-further/hosted-vs-handler) — where auth pages live * [All auth providers](./auth-providers) — per-provider setup * [User fundamentals](/guides/getting-started/user-fundamentals) — the user object, metadata, and sessions * [Ship production-ready auth](/guides/other/tutorials/ship-production-ready-auth) — hardening walkthrough # JWT Tokens Source: https://docs.hexclave.com/guides/apps/authentication/jwts Understand how Hexclave uses JSON Web Tokens for authentication JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties. Hexclave uses JWTs for secure authentication and authorization. You don't need to worry about JWTs if you're using Hexclave. However, if you are an expert user and want the full flexibility to manually verify JWTs for performance or other reasons, this page is for you. ## What is a JWT? A JWT is a string that consists of three parts separated by dots (`.`): 1. **Header**: Contains metadata about the token, such as the signing algorithm 2. **Payload**: Contains the claims (data) about the user or entity 3. **Signature**: Used to verify the token's authenticity The structure looks like this: `header.payload.signature` ## Hexclave JWT Structure Hexclave JWTs contain standardized headers and claims that power authentication throughout the platform. ### Header * **`alg`**: Always `ES256` * **`kid`**: Identifies which public key from the JWKS should be used for verification ### Standard Claims * **`iss` (Issuer)**: `https://api.hexclave.com/api/v1/projects/` for regular users, or `https://api.hexclave.com/api/v1/projects-anonymous-users/` for anonymous sessions * **`sub` (Subject)**: The user ID this token represents * **`aud` (Audience)**: The intended recipient of the token — `` for regular sessions, `:anon` for anonymous sessions * **`exp` (Expiration)**: When the token expires (Unix timestamp) * **`iat` (Issued At)**: When the token was issued (Unix timestamp) ### Hexclave Specific Claims * **`project_id`**: Your Hexclave project ID * **`branch_id`**: The project branch (currently always `main`) * **`refresh_token_id`**: ID of the associated refresh token * **`role`**: Always set to `authenticated` for valid users * **`name`**: The user's display name (nullable) * **`email`**: The user's primary email address (nullable) * **`email_verified`**: Whether the user's email has been verified * **`selected_team_id`**: The currently selected team ID (nullable) * **`is_anonymous`**: Whether this is an anonymous user session * **`is_restricted`**: Whether the user is restricted (e.g., unverified email, anonymous, or restricted by an administrator) * **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is `anonymous`, `email_not_verified`, or `restricted_by_administrator` ## Example JWT Payload Here's what a typical Hexclave JWT payload looks like: ```json theme={null} { "iss": "https://api.hexclave.com/api/v1/projects/project_abcdef", "sub": "user_123456", "aud": "project_abcdef", "exp": 1735689600, "iat": 1735603200, "project_id": "project_abcdef", "branch_id": "main", "refresh_token_id": "refresh_xyz789", "requires_totp_mfa": false, "role": "authenticated", "name": "John Doe", "email": "john@example.com", "email_verified": true, "selected_team_id": "team_789", "is_anonymous": false, "is_restricted": false, "restricted_reason": null } ``` Anonymous user tokens have the same shape, but: * `iss` becomes `https://api.hexclave.com/api/v1/projects-anonymous-users/` * `aud` becomes `:anon` * `is_anonymous` is `true` * `is_restricted` is `true` * `restricted_reason` is `{ "type": "anonymous" }` Restricted user tokens (e.g., users who haven't verified their email when verification is required) have: * `iss` becomes `https://api.hexclave.com/api/v1/projects-restricted-users/` * `aud` becomes `:restricted` * `is_restricted` is `true` * `restricted_reason` is `{ "type": "email_not_verified" }` Users restricted by an administrator (e.g., via [sign-up rules](/guides/apps/authentication/sign-up-rules)) have the same structure: * `iss` becomes `https://api.hexclave.com/api/v1/projects-restricted-users/` * `aud` becomes `:restricted` * `is_restricted` is `true` * `restricted_reason` is `{ "type": "restricted_by_administrator" }` ## Working with JWTs ### Client-Side Usage Hexclave automatically handles JWT tokens for you. When you use hooks like `useUser()`, the JWT is automatically included in API requests: **Next.js:** ```tsx theme={null} import { useUser } from '@hexclave/next'; export function UserProfile() { const user = useUser(); if (!user) { return
Please sign in
; } return
Welcome, {user.displayName}!
; } ``` **React:** ```tsx theme={null} import { useUser } from '@hexclave/react'; export function UserProfile() { const user = useUser(); if (!user) { return
Please sign in
; } return
Welcome, {user.displayName}!
; } ``` ### Server-Side Usage On the server side, you can access the JWT and its claims through the Hexclave API: ```typescript theme={null} import { hexclaveServerApp } from '@/stack'; export async function GET() { const user = await hexclaveServerApp.getUser(); if (!user) { return new Response('Unauthorized', { status: 401 }); } // Access user information from the JWT return Response.json({ id: user.id, displayName: user.displayName, primaryEmail: user.primaryEmail, selectedTeamId: user.selectedTeamId, // Other user properties... }); } ``` ### Manual JWT Verification If you need to manually verify a JWT (for example, in a different service), fetch the public keys from Hexclave's JWKS endpoint. Keys are derived per audience so the `kid` in the JWT header always matches one of the published keys. ```typescript theme={null} import * as jose from 'jose'; // Get the public key set from Hexclave const jwks = jose.createRemoteJWKSet( new URL('https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json') ); // Verify a regular (non-anonymous) access token try { const { payload } = await jose.jwtVerify(token, jwks, { issuer: 'https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID', audience: 'YOUR_PROJECT_ID', }); console.log('JWT is valid:', payload); } catch (error) { console.error('JWT verification failed:', error); } ``` To support anonymous sessions, include those keys and allow both issuers and audiences: ```typescript theme={null} import * as jose from 'jose'; const jwks = jose.createRemoteJWKSet( new URL('https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true') ); const { payload } = await jose.jwtVerify(token, jwks, { issuer: [ 'https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID', 'https://api.hexclave.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', ], audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'], }); ``` To support restricted users (e.g., users who haven't verified their email), add `include_restricted=true`: ```typescript theme={null} import * as jose from 'jose'; const jwks = jose.createRemoteJWKSet( new URL('https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true') ); // All three user types have different issuers const { payload } = await jose.jwtVerify(token, jwks, { issuer: [ 'https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID', 'https://api.hexclave.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', 'https://api.hexclave.com/api/v1/projects-restricted-users/YOUR_PROJECT_ID', ], audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'], }); ``` ### Signing Keys * Private keys are deterministically derived from your project ID, optional anonymous audience, and the `STACK_SERVER_SECRET` environment variable. This means no key material is ever stored in the database. * The JWKS currently exposes both the latest key pair and a legacy compatibility key. Verification libraries automatically pick the correct key by matching the `kid` provided in the JWT header. * Tokens are always signed server-side; client SDKs never receive the private keys. ## Security Considerations ### Token Storage * **Never store JWTs in localStorage** for sensitive applications * Use secure, httpOnly cookies when possible * Hexclave handles secure token storage automatically ### Token Expiration * JWTs have a limited lifetime (default is 10 minutes via `STACK_ACCESS_TOKEN_EXPIRATION_TIME`) * Hexclave automatically refreshes tokens before they expire * Always check the `exp` claim when manually handling JWTs ### Signature Verification * Always verify JWT signatures using the public key * Never trust the contents of a JWT without verification * Hexclave SDKs handle verification automatically ## Troubleshooting ### Common Issues 1. **"JWT is expired"**: The token has passed its expiration time. Hexclave will automatically refresh it. 2. **"Invalid signature"**: The token was tampered with or signed with a different key. 3. **"Invalid audience"**: The token was issued for a different project or environment. ### Debugging JWTs Use a JWT viewer such as [jwt.io](https://jwt.io/) to inspect tokens and verify their contents. Pay special attention to: * Expiration times (`exp` claim) * Audience (`aud` claim) matching your project * Required claims are present ## Best Practices 1. **Let Hexclave handle tokens**: Use the provided SDKs instead of manual JWT handling 2. **Validate on the server**: Always verify JWTs on your backend 3. **Check expiration**: Ensure tokens haven't expired before using them 4. **Use HTTPS**: Always transmit JWTs over secure connections 5. **Monitor token usage**: Log authentication events for security monitoring ## Related Concepts * [API Keys](/guides/apps/api-keys/overview) - Alternative authentication method for server-to-server communication * [Setup](/guides/getting-started/setup) - How to verify user sessions in your backend * [Permissions](/guides/apps/rbac/overview) - Understanding user permissions (not included in JWTs) * [Teams](/guides/apps/teams/overview) - Understanding team context in JWTs # Authentication Source: https://docs.hexclave.com/guides/apps/authentication/overview Production-ready sign-in, a real user directory, and session verification - without building auth yourself **For agents/LLMs:** This is a high-level *marketing* overview of the Authentication app, not an implementation reference. To actually build auth, use [Setup](/guides/getting-started/setup) and the [Authentication guide](./guide) (sign-in methods, auth pages, session reads, and route protection), then the deeper pages: [Hosted vs. Handler](/guides/going-further/hosted-vs-handler), [Auth providers](./auth-providers), [JWTs & session verification](./jwts), [Sign-up rules](./sign-up-rules), and [Connected accounts](./connected-accounts). Authentication is the foundation every other Hexclave app builds on. You get hosted sign-in pages, every common login method, and a complete user directory - wired up in minutes, not weeks. Below are the questions developers actually ask, and the honest answers. ## Can I add sign-in without building forms? Yes. Prefer **hosted components** (`urls: { default: { type: "hosted" } }` in [Setup](/guides/getting-started/setup)) — Hexclave hosts the sign-in UI and keeps it updated. Or mount `` on your own `/handler/[...]` route if you want auth pages on your domain. See [Hosted vs. Handler](/guides/going-further/hosted-vs-handler). ```tsx title="app/handler/[...hexclave]/page.tsx" theme={null} import { HexclaveHandler } from "@hexclave/next"; // replace `next` with your framework SDK export default function Handler() { return ; } ``` You never write a form, manage a redirect, or hand-roll a reset flow. Want it inside your own layout? Drop in the prebuilt components - ``, ``, `` - and tune them with props. Want a fully custom UI? Build your own against the SDK's auth methods. ## Can I offer every login method? Yes - and you flip each one on or off from the dashboard, no redeploy required. * **Email & password** with secure reset * **Magic links / OTP** for passwordless sign-in * **Passkeys** (WebAuthn) for phishing-resistant login * **12 OAuth providers** - Google, GitHub, Microsoft, Apple, Discord, Facebook, LinkedIn, Twitch, Spotify, GitLab, Bitbucket, and X - plus your own OpenID Connect provider * **Two-factor authentication** (TOTP) Shared Hexclave keys work out of the box for Google, GitHub, Microsoft, and Spotify — best for a [development environment](/guides/going-further/local-vs-cloud-dashboard). Swap in your own client ID and secret for production. See [Auth Providers → Shared vs. Custom OAuth Keys](./auth-providers#shared-vs-custom-oauth-keys). ## Can I get the current user anywhere in my app? Yes. The same user object is available on the client (as a hook) and the server (as an async call), with full TypeScript types. ```tsx theme={null} // Client component - re-renders when the user changes const user = useUser(); // Server component / route handler / action const user = await hexclaveServerApp.getUser(); ``` Need to gate a page? Pass `{ or: "redirect" }` and unauthenticated visitors are sent to sign-in automatically: ```tsx theme={null} const user = useUser({ or: "redirect" }); ``` ## Can I manage my users? Yes. Every sign-up creates a real profile - connected accounts, auth methods, metadata, and activity - not just a token. Search, filter, edit, and export from the dashboard, or do the same from code with the server SDK. Store your own data on a user with `clientMetadata`, `clientReadOnlyMetadata`, and `serverMetadata`, so you rarely need a separate users table of your own. ## Can I verify sessions on my backend? Yes. Hexclave issues standard JWTs you can verify locally against a JWKS endpoint - no round-trip to Hexclave on every request - so auth checks stay fast even in middleware and edge functions. See [JWTs & session verification](./jwts). ## Can I control who gets in? Yes. Write [sign-up rules](./sign-up-rules) to allow, reject, or restrict accounts by email domain, country, auth method, or built-in [fraud-protection](./fraud-protection) risk scores. Suspicious accounts can be held in a [restricted](./restricted-users) state for review instead of blocked outright. ## Can I own my data and self-host? Yes. Hexclave is open source (MIT client, AGPLv3 server). Run it fully self-hosted, export your users from the dashboard whenever you want, and avoid lock-in. The same SDK and APIs work on the managed service and self-hosted. A [development environment](/guides/going-further/local-vs-cloud-dashboard) is for building against Hexclave locally — some production-only setup (custom OAuth keys, custom email, payments) lives in the [cloud dashboard](https://app.hexclave.com). ## Start here 1. [Set up Hexclave](/guides/getting-started/setup) in your project (a few minutes) — prefer hosted components; see [Hosted vs. Handler](/guides/going-further/hosted-vs-handler). 2. Turn on the auth methods you want (and mount `` only if you chose the own-handler path). 3. Use `useUser()` / `getUser()` to read the session and protect routes. Everything else - [teams](../teams/overview), [payments](../payments/overview), [emails](../emails/overview), [analytics](../analytics/overview) - keys off this same user directory. Ready for a start-to-finish walkthrough — sign-in methods, auth pages, reading the user, and protecting routes? Read the [Authentication guide](./guide). # Restricted Users Source: https://docs.hexclave.com/guides/apps/authentication/restricted-users Understand and handle users with limited access Restricted users are signed-in users whose account exists, but has not been granted normal application access yet. Stack marks these users with `user.isRestricted === true` and provides a `user.restrictedReason` explaining why. By default, Hexclave treats restricted users like unauthenticated users in most SDK calls. This prevents accounts that still need verification, review, or conversion from accidentally getting access to protected product flows. ## When users are restricted Users can be restricted for a few reasons: * **Email not verified**: the project requires email verification before full access. * **Anonymous user**: anonymous users can interact with the app, but are always restricted until converted. * **Restricted by administrator**: the user was restricted manually or by a [sign-up rule](/guides/apps/authentication/sign-up-rules). You can inspect the reason from the SDK: ```ts my-app.ts theme={null} import { hexclaveServerApp } from "../src/stack/server"; const user = await hexclaveServerApp.getUser({ includeRestricted: true }); if (user?.isRestricted) { console.log(user.restrictedReason?.type); } ``` The current `restrictedReason.type` values are: | Type | Meaning | | ----------------------------- | ------------------------------------------------------ | | `email_not_verified` | The user still needs to verify their email address. | | `anonymous` | The user is an anonymous user. | | `restricted_by_administrator` | The user was restricted manually or by a sign-up rule. | ## Loading restricted users Most calls exclude restricted users unless you explicitly opt in. Use `includeRestricted: true` when you are building onboarding, email verification, account review, or anonymous-user conversion flows. ```ts my-app.ts theme={null} import { hexclaveServerApp } from "../src/stack/server"; const user = await hexclaveServerApp.getUser({ includeRestricted: true }); if (user?.isRestricted) { console.log("Needs onboarding:", user.restrictedReason?.type); } ``` ```tsx my-react-component.tsx theme={null} "use client"; import { hexclaveClientApp } from "../src/stack/client"; export default function OnboardingGate() { const user = hexclaveClientApp.useUser({ includeRestricted: true }); if (!user) { return Sign in; } if (user.isRestricted) { return ; } return
Welcome back, {user.displayName ?? user.primaryEmail}
; } ```
Anonymous users are restricted by definition. Passing `{ or: "anonymous" }` automatically includes restricted users, and cannot be combined with `{ includeRestricted: false }`. ## Handling restricted users Treat restricted users as a separate state from both "signed out" and "fully signed in". A good default is to show a page that tells the user what they need to do next. ```tsx restricted-user-message.tsx theme={null} function RestrictedUserMessage({ reason }: { reason: string | undefined }) { if (reason === "email_not_verified") { return
Please verify your email address to continue.
; } if (reason === "anonymous") { return Create an account to save your progress.; } return
Your account is waiting for review.
; } ``` For API routes or backend actions, keep using the default behavior unless the endpoint is specifically meant to serve restricted users. This helps prevent partially onboarded accounts from reaching product APIs. ## Restricted users in JWTs Restricted users receive tokens with `is_restricted` and `restricted_reason` claims. If your backend verifies Hexclave JWTs directly, make sure you reject restricted users unless the endpoint intentionally supports them. When fetching Hexclave's JWKS, restricted-user signing keys are excluded by default. Include them only for services that intentionally accept restricted users: ```txt jwks-url.txt theme={null} /.well-known/jwks.json?include_restricted=true ``` If you also accept anonymous users, use `include_anonymous=true`; anonymous keys imply restricted-user keys. ## Admin and sign-up review Sign-up rules can restrict a user instead of rejecting the sign-up outright. This is useful when you want the account to exist, but need manual review before granting access. For more details on creating rules that restrict users, see [Sign-up Rules](/guides/apps/authentication/sign-up-rules). # Sign-up Rules Source: https://docs.hexclave.com/guides/apps/authentication/sign-up-rules Control who can sign up for your application with customizable rules. Sign-up rules let you control who can sign up for your application. You can create rules that evaluate sign-up attempts based on conditions like email domain or authentication method, then allow, reject, or restrict users accordingly. Rules are evaluated during sign-up for all authentication methods (password, magic link, OAuth, passkey). When a user attempts to sign up, Stack evaluates each rule in priority order—the first matching rule determines the outcome. If no rules match, the default action is used. ## Creating rules Navigate to **Sign-up Rules** in your project dashboard to create and manage rules. To add a rule: 1. Click **Add Rule** 2. Enter a name for your rule (e.g., "Block disposable emails") 3. Configure the conditions using the visual builder 4. Select an action (Allow, Reject, Restrict, or Log) 5. Click **Create rule** ### Available conditions When building rule conditions, you have access to these context variables: | Variable | Type | Description | | --------------- | ------ | --------------------------------------------------------------------------------------- | | `email` | string | The user's email address (normalized to lowercase) | | `emailDomain` | string | The domain part of the email (after @) | | `authMethod` | string | The authentication method: `password`, `otp`, `oauth`, or `passkey` | | `oauthProvider` | string | The OAuth provider ID if using OAuth (e.g., `google`, `github`), empty string otherwise | The condition builder supports these operations on string values: * `contains("substring")` - Check if value contains a substring * `startsWith("prefix")` - Check if value starts with a prefix * `endsWith("suffix")` - Check if value ends with a suffix * `matches("regex")` - Check if value matches a regular expression * `==` and `!=` - Exact equality comparisons You can combine multiple conditions using AND/OR logic. ## Actions ### Allow The user signs up normally. Use this to explicitly allow certain users when your default action is set to reject. ### Reject Blocks the sign-up and shows the user: "Your sign up was rejected by an administrator's sign-up rule." You can optionally add an internal message for logging (not shown to users). ### Restrict The user signs up, but their account is marked as restricted. Restricted users have limited access and can be reviewed by admins before gaining full access. See [JWT Tokens](/guides/apps/authentication/jwts) for how restricted status appears in tokens. ### Log The rule is triggered and logged for analytics, but no action is taken. Use this to monitor patterns before implementing blocking rules. ## Priority and default action Rules are evaluated in priority order (highest first). You can reorder rules by dragging them in the dashboard. Only the first matching rule's action is applied, so place your allow rules before reject rules if you want to allow specific users while blocking others. The default action applies when no rules match: * **Allow** (default): Sign-ups are allowed unless a rule explicitly rejects them * **Reject**: Sign-ups are blocked unless a rule explicitly allows them Set the default to "Reject" when you want to only allow sign-ups from specific domains. ## Common use cases ### Block disposable email domains Block users signing up with temporary email addresses: * Condition: `emailDomain.matches("(tempmail|throwaway|guerrillamail)\\..*")` * Action: Reject ### Allow only corporate domains 1. Set default action to **Reject** 2. Create an allow rule with condition: `emailDomain == "company1.com" || emailDomain == "company2.com"` ### Restrict non-verified auth methods Require manual review for users who sign up without email verification: * Condition: `authMethod == "oauth" && oauthProvider != "google"` * Action: Restrict ### Different rules for different auth methods Allow password sign-ups from any domain, but restrict OAuth sign-ups: 1. Rule 1: `authMethod == "password"` → Allow 2. Rule 2: `authMethod == "oauth"` → Restrict 3. Default: Allow ## Analytics The dashboard shows analytics for each rule, including how many times it's been triggered over the past 48 hours. Use this to understand your sign-up patterns and tune your rules. ## Testing rules You can test your sign-up rules using the built-in rule tester. It simulates sign-up requests and shows which rules would trigger and what the outcome would be—without affecting real users. To open the tester, scroll to the bottom of the Sign-up Rules page and click **Open tester**. ### Test inputs Enter the following to simulate a sign-up attempt: * **Email**: The email address to test (e.g., `user@company.com`) * **Auth method**: The authentication method (`Password`, `OTP`, `OAuth`, or `Passkey`) * **OAuth provider**: The OAuth provider ID (only used when auth method is OAuth) Click **Run test** to see the results. ### Understanding the results The tester displays: * **Outcome**: Whether the sign-up would be allowed or rejected, and whether the decision came from a specific rule or the default action. * **Triggered rules**: All rules that matched the test input, showing each rule's name, condition, action type, and whether it was the deciding rule. * **Evaluation trace**: A detailed view of how every rule was evaluated—which matched, which didn't, which were disabled, and any errors. * **Normalized context**: How the test input was parsed, including the extracted email domain. Useful for debugging conditions that reference `email`, `emailDomain`, `authMethod`, or `oauthProvider`. # User Onboarding Source: https://docs.hexclave.com/guides/apps/authentication/user-onboarding Implementing a user onboarding page and collecting information on sign-up Sometimes, you may want to collect additional information from users during sign-up, for example a real name or address. The most straightforward approach is to redirect users to an onboarding page right after they sign up. However, this is not recommended for the following reasons: 1. Users can accidentally (or purposefully) close or navigate away from the page before completing the onboarding. 2. Redirect URLs may vary depending on the context. For instance, if a user is redirected to a sign-in page after trying to access a protected page, they'll expect to return to the original protected page post-authentication. Instead, a more reliable strategy is to store an `onboarded` flag in the user's metadata and redirect users to the onboarding page if they haven't completed it yet. ## Example implementation Let's say you have an onboarding page that asks for an address and stores it in the user's [metadata](/guides/getting-started/user-fundamentals#custom-metadata): ```jsx theme={null} export default function OnboardingPage() { const user = useUser(); const router = useRouter(); const [address, setAddress] = useState(''); return <> setAddress(e.target.value)} /> ); } ``` While the above implementation offers a basic onboarding process, users can still skip onboarding by directly sending an API request to update the `clientMetadata.onboarded` flag. If you want to ensure that onboarding cannot be bypassed on the API level, you should create a server endpoint to validate and store the data, then save the `onboarded` flag in the `clientReadOnlyMetadata` on the server side after validation. Next, we can create a hook/function to check if the user has completed onboarding and redirect them to the onboarding page: ```jsx theme={null} 'use client'; import { useEffect } from 'react'; import { useUser } from '@hexclave/next'; // replace `next` with the correct framework SDK package import { useRouter } from 'next/navigation'; export function useOnboarding() { const user = useUser(); const router = useRouter(); useEffect(() => { if (!user.clientReadOnlyMetadata.onboarded) { router.push('/onboarding'); } }, [user]); } ``` ```jsx theme={null} import { hexclaveServerApp } from '@/hexclave/server'; import { redirect } from 'next/navigation'; export async function ensureOnboarded() { const user = await hexclaveServerApp.getUser(); if (!user.clientReadOnlyMetadata.onboarded) { redirect('/onboarding'); } } ``` You can then use these functions wherever onboarding is required: ```jsx theme={null} import { useOnboarding } from '@/app/onboarding-hooks'; import { useUser } from '@hexclave/next'; // replace `next` with the correct framework SDK package export default function HomePage() { useOnboarding(); const user = useUser(); return (
Welcome to the app, {user.displayName}
); } ```
```jsx theme={null} import { ensureOnboarding } from '@/app/onboarding-functions'; import { hexclaveServerApp } from '@/hexclave/server'; export default async function HomePage() { await ensureOnboarding(); const user = await hexclaveServerApp.getUser(); return (
Welcome to the app, {user.displayName}
); } ```
# Data Vault Source: https://docs.hexclave.com/guides/apps/data-vault/overview An encrypted key-value store for sensitive data, with zero-knowledge security Data Vault is an encrypted key-value store built into Hexclave. It lets you securely store sensitive data — API tokens, connection strings, secrets, or any other values — without ever exposing plaintext to Hexclave's database or operators. ## How it works Data Vault uses a **double encryption** design: 1. **Client-side encryption** — Your SDK encrypts values and hashes keys locally before they leave your server, using a secret that only you know. Hexclave never sees your plaintext keys or values. 2. **Server-side encryption** — Hexclave adds a second layer of envelope encryption using a rotating master key, so even the encrypted data at rest is further protected. Because keys are hashed before storage, **you cannot list or enumerate keys** in a store. You must know the exact key to retrieve a value. If you lose your secret, your data is unrecoverable. Even Hexclave cannot decrypt your values without it. Keep your secret safe and backed up. ## Setup ### 0. Enable the Data Vault app Before you can create stores, you need to enable the Data Vault app for your project: 1. Open your Hexclave dashboard 2. Go to **Apps** 3. Find and open **Data Vault** 4. Click **Enable** ### 1. Create a store Go to your project's **Data Vault → Stores** page in the [Hexclave dashboard](https://app.hexclave.com) and click **Create Store**. Each project can have **multiple stores**, and each store is fully isolated from the others. When creating a store you'll be asked for: * **Store ID** (required) — the identifier you'll reference in your code. Must contain only letters, numbers, underscores, and hyphens, and cannot start with a hyphen. Store IDs are immutable once created. * **Display Name** (optional) — a human-readable label shown in the dashboard. Defaults to `Store ` if left blank. Unlike the ID, the display name can be edited later from the store detail page. Stores are stored in your project config under `dataVault.stores.` and are part of your pushable configuration, so they propagate across branches like any other config setting. ### Managing a store Click any store in the **Stores** list to open its detail page. From there you can: * **Copy the Store ID** — useful when wiring it into your code or environment * **Rename the store** — edit the Display Name and click **Save** to persist * **Delete the store** — click **Delete Store**, then type the store ID into the confirmation dialog to confirm. **Deletion is irreversible**: all encrypted data in the store is permanently deleted, and Hexclave cannot recover it. Deleting a store cannot be undone. Make sure no production traffic references the store ID before removing it. ### 2. Generate a secret Your secret can be any string, but for strong security it should be at least 32 characters long and provide 256 bits of entropy. Store it as an environment variable: ```bash title=".env" theme={null} HEXCLAVE_DATA_VAULT_SECRET=your-randomly-generated-secret-here ``` ### 3. Use the SDK Data Vault is accessed through the **server app** only — it requires your secret server key. ```typescript title="server-example.ts" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; const store = await hexclaveServerApp.getDataVaultStore("my-store-id"); const key = user.id; // Store a value await store.setValue(key, "my-sensitive-value", { secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); // Retrieve a value const value = await store.getValue(key, { secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); // value is the decrypted string, or null if the key doesn't exist ``` ## API reference ### `getDataVaultStore(id)` Returns a `DataVaultStore` object for the given store ID. The store must already exist in your project config (created via the dashboard). ```typescript theme={null} const store = await hexclaveServerApp.getDataVaultStore("my-store-id"); ``` ### `store.getValue(key, { secret })` Retrieves the decrypted value for the given key, or `null` if the key doesn't exist. ```typescript theme={null} const value = await store.getValue("some-key", { secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); ``` ### `store.setValue(key, value, { secret })` Stores an encrypted value for the given key. If the key already exists, it is overwritten. ```typescript theme={null} await store.setValue("some-key", "some-value", { secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); ``` ## Security model * **Keys** are hashed with an iterated hash (100,000 iterations) derived from your secret and the logical key. The server only stores the hash. * **Values** are encrypted client-side using a derived key from the same secret + key pair, then re-encrypted server-side with KMS envelope encryption. * **Your secret** never leaves your server. Hexclave's API only receives hashed keys and double-encrypted values. * **No enumeration** — since only hashed keys are stored, there is no way to list all keys in a store. This is a deliberate security property. ## Use cases * **Storing third-party API tokens** — safely persist user-specific tokens for external services * **Connection strings** — store database or service connection strings per-tenant * **Encryption keys** — use Data Vault as a key store for your own application-level encryption * **Any sensitive per-user data** — anything you don't want in plaintext metadata fields # Drafts Source: https://docs.hexclave.com/guides/apps/emails/drafts Compose emails in the dashboard, then send from the UI or with draftId. Drafts are emails you compose in the Hexclave dashboard — not TSX files like [templates and themes](./templates-and-themes). Use them for one-off or campaign-style sends when you don't want to ship a template in code. ## Create a draft Open **Emails → Drafts** in the dashboard: 1. Create a blank draft, or **clone a template into a draft** to start from existing content. 2. Edit the body (including TSX source with live preview), pick a theme, choose recipients, and optionally schedule a send time. 3. Save. Active drafts stay editable until you send them; sent drafts move to history and can be tracked in the outbox. You can create and edit drafts while on the shared email server. On Shared, programmatic sends still go out wrapped as Hexclave dev emails; for production sender identity, configure SMTP, Resend, or Managed in the [cloud dashboard](https://app.hexclave.com). The dashboard may ask you to configure a custom server before sending manual mail from the UI. ## Send a draft Send from the dashboard when you're ready, or trigger the same draft from your backend: ```typescript theme={null} await hexclaveServerApp.sendEmail({ userIds: ["user-id"], draftId: "your-draft-id", }); ``` `draftId` is one of the three content options on `sendEmail` (alongside `html` and `templateId`). Recipients, scheduling, and theme overrides work the same as any other send — see the [Emails guide](./guide). ## Related * [Templates & themes](./templates-and-themes) - reusable TSX content and branding wrappers. * [Emails guide](./guide) - full `sendEmail` options, delivery, and server configuration. * [Emails overview](./overview) - high-level product overview. # Emails Source: https://docs.hexclave.com/guides/apps/emails/guide Implement Hexclave emails from start to finish — server, templates, sending, and delivery. This guide walks through implementing emails in your app end to end: connect a server, brand your mail, create a template, send from your backend, then watch delivery. For a quick "can I do this?" checklist, see the [Emails overview](./overview). You'll need Hexclave set up with a server app (`hexclaveServerApp`). If you don't have that yet, follow [Setup](/guides/getting-started/setup) first, then enable **Emails** in the dashboard. ## 1. Connect an email server Open **Emails → Email Settings** in the dashboard. Hexclave needs somewhere to send from. **While developing**, leave **Shared** selected. Built-in auth mail (verification, password reset, magic link) already works. Custom `sendEmail` calls also go out on shared — Hexclave wraps them as "dev emails" so recipients know they aren't from your domain yet. A [development environment](/guides/going-further/local-vs-cloud-dashboard) can only use Shared. Configure the other providers in the [cloud dashboard](https://app.hexclave.com). **Before production**, switch to one of: | Provider | What you do | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Custom SMTP** | Enter host, port, username, password, sender name, and sender email (SendGrid, Postmark, SES, etc.). | | **Resend** | Paste your [Resend](https://resend.com) API key. | | **Managed** | Pick a subdomain (e.g. `mail.yourapp.com`) and sender local part. Hexclave shows DNS records during onboarding — you add them at your DNS provider, then verify. Hexclave handles signing and deliverability. | Saving a custom provider triggers a test email from the dashboard so you know the config works. ## 2. Pick transactional vs marketing Every send is one of two categories: * **Transactional** — required for the product (verification, receipts, password reset). Users cannot opt out. * **Marketing** — promotional or informational. Users can unsubscribe; Hexclave appends an unsubscribe link. Never send marketing content as transactional mail. That can get your domain blacklisted. Set the category when you send (`notificationCategoryName`) or inside the template / draft with ``. If you omit it everywhere, the category stays undefined and **unsubscribe preferences are not applied** — prefer setting it explicitly. ## 3. Brand with a theme Themes wrap every email (header, footer, logo, background). Hexclave ships **Default Light**, **Default Dark**, and **Default Colorful**. Set a project default under **Emails → Email Settings → Themes**, or skip this step and use the default. To author your own theme as TSX: ```tsx theme={null} import { Html, Head, Tailwind, Body, Container } from "@react-email/components"; import { ThemeProps, ProjectLogo } from "@hexclave/emails"; export function EmailTheme({ children, unsubscribeLink, projectLogos }: ThemeProps) { return ( {children} {unsubscribeLink && (

Unsubscribe

)}
); } ``` Per-send overrides: `themeId: "your-theme-id"`, `themeId: null` for the project default, or `themeId: false` for no theme. Full theme API: [Templates & themes](./templates-and-themes). ## 4. Create a template Built-in templates already cover verification, password reset, magic link, invitations, payment receipts, payment failures, and trial-ending notices — Hexclave sends those automatically when those flows run. Customize them under **Emails → Templates**. For your own product email, create a React Email template in the dashboard (or start from a clone). A minimal template looks like this: ```tsx theme={null} import { type } from "arktype"; import { Container } from "@react-email/components"; import { Subject, NotificationCategory, Props } from "@hexclave/emails"; export const variablesSchema = type({ featureName: "string", }); export function EmailTemplate({ user, variables, }: Props) { return (

Hi {user.displayName}, check out {variables.featureName}!

); } EmailTemplate.PreviewVariables = { featureName: "Dark mode", } satisfies typeof variablesSchema.infer; ``` * `variablesSchema` validates the variables you pass at send time. * `` and `` live in the template so the content owns its subject and category. * Saving custom templates on Shared requires a custom email server; you can still edit and preview. Don't want a reusable template yet? You can send raw `html` in the next step, or compose a [Draft](./drafts) in the dashboard instead. ## 5. Send your first email From your server, call `hexclaveServerApp.sendEmail()`. Exactly one recipient selector and exactly one content source are required. **HTML (fastest smoke test):** ```typescript theme={null} import { hexclaveServerApp } from "@hexclave/next"; // replace `next` with your framework SDK await hexclaveServerApp.sendEmail({ userIds: ["user-id"], subject: "Welcome aboard!", html: "

Welcome!

Thanks for joining us.

", notificationCategoryName: "Transactional", }); ``` **Template with variables:** ```typescript theme={null} await hexclaveServerApp.sendEmail({ userIds: ["user-id"], templateId: "your-template-id", variables: { featureName: "Dark mode" }, // subject / category can come from the template; override here if needed }); ``` **Everyone in the project:** ```typescript theme={null} await hexclaveServerApp.sendEmail({ allUsers: true, templateId: "your-template-id", subject: "We just shipped a big update", variables: { featureName: "Dark mode" }, notificationCategoryName: "Marketing", }); ``` **Dashboard draft:** ```typescript theme={null} await hexclaveServerApp.sendEmail({ userIds: ["user-id"], draftId: "your-draft-id", }); ``` ### Options reference ```typescript theme={null} type SendEmailOptions = & { subject?: string; themeId?: string | null | false; notificationCategoryName?: string; variables?: Record; scheduledAt?: Date; } & ({ userIds: string[] } | { allUsers: true }) & ({ html: string } | { templateId: string } | { draftId: string }); ``` `sendEmail` resolves to `void` and **throws** on failure. Branch on stable `errorCode` values when present: ```typescript theme={null} try { await hexclaveServerApp.sendEmail({ userIds: ["user-id"], html: "

Hello!

", subject: "Test Email", notificationCategoryName: "Transactional", }); } catch (error) { const errorCode = (error as { errorCode?: string }).errorCode; switch (errorCode) { case "USER_ID_DOES_NOT_EXIST": // One or more user IDs do not exist break; case "SCHEMA_ERROR": // Invalid email data provided break; default: throw error; // rethrow anything you didn't explicitly handle } } ``` Unknown `templateId` or `draftId` values fail the request immediately (HTTP 400) — nothing is enqueued. Those errors typically have no `errorCode` in the switch above, so they fall through to `default`. ## 6. Schedule a send (optional) Pass `scheduledAt` to enqueue now and deliver later. Omit it to send as soon as the pipeline allows. ```typescript theme={null} await hexclaveServerApp.sendEmail({ userIds: ["user-id"], html: "

Happy New Year!

", subject: "Happy New Year!", notificationCategoryName: "Marketing", scheduledAt: new Date("2027-01-01T00:00:00Z"), }); ``` ## 7. Watch delivery After `sendEmail` returns, the message shows up under **Emails → Sent** with a status such as Preparing, Rendering, Scheduled, Queued, Sending, Sent, Skipped, Render Error, or Server Error. Under the hood, Hexclave enqueues, renders, queues (respecting capacity and `scheduledAt`), sends (honoring unsubscribes), and tracks delivery. From code: ```typescript theme={null} const info = await hexclaveServerApp.getEmailDeliveryStats(); // info.stats.day.sent, info.stats.day.bounced, info.stats.day.marked_as_spam // info.capacity.rate_per_second, info.capacity.is_boost_active, ... ``` Stats cover hour, day, week, and month windows for **sent**, **bounced**, and **marked as spam**. If you need a short-term throughput increase, call it from your server: ```typescript theme={null} await hexclaveServerApp.activateEmailCapacityBoost(); ``` Or increase capacity from the dashboard: open **Emails → Sent**, find the **Domain Reputation** card, and click **Temporarily increase capacity**. A boost raises hourly capacity for a limited time (about 4× for 4 hours). It still counts against your overall monthly sending capacity. ## 8. Optional: compose without a code template For one-off or campaign mail, open **Emails → Drafts**, create a blank draft or clone a template, edit with live preview, pick a theme and recipients, then send from the UI or with `draftId`. Details: [Drafts](./drafts). ## What you should have now 1. An email server (Shared for development, custom for production) 2. A theme (built-in or your own) 3. A template, HTML body, or draft 4. At least one successful `sendEmail` from your backend 5. Visibility into delivery in **Emails → Sent** / `getEmailDeliveryStats` Built-in auth and payment emails continue to send automatically when those flows run — you only call `sendEmail` for your own product mail. ## Related * [Emails overview](./overview) — capability FAQ * [Templates & themes](./templates-and-themes) — full template and theme APIs * [Drafts](./drafts) — dashboard composition and `draftId` * [Local vs cloud dashboard](/guides/going-further/local-vs-cloud-dashboard) — where Shared vs custom providers apply # Emails Source: https://docs.hexclave.com/guides/apps/emails/overview Send branded transactional and marketing email - rendering, delivery, and tracking handled for you **For agents/LLMs:** This is a high-level *marketing* overview of the Emails app, not an implementation reference. To actually build with email, use [Setup](/guides/getting-started/setup) and the [Emails guide](./guide) (sending, templates, themes, delivery, and server configuration). The Emails app covers Hexclave's built-in mail (verification, password resets, receipts) and your own transactional or marketing sends — templates, themes, scheduling, unsubscribes, and delivery tracking included. Below are the questions developers actually ask, and the honest answers. ## Can I send an email from my backend? Yes. One call from your server, addressed to specific users or everyone in your project. No SMTP wiring, no render step to manage. ```typescript theme={null} await hexclaveServerApp.sendEmail({ userIds: ["user-id"], subject: "Welcome aboard!", html: "

Welcome!

Thanks for joining us.

", }); ``` Send raw HTML, a template with variables, or a draft you composed in the dashboard. And failures are never silent - `sendEmail` throws on error with a stable `errorCode` (like `REQUIRES_CUSTOM_EMAIL_SERVER` or `USER_ID_DOES_NOT_EXIST`) so you can handle exactly the cases you care about. ## Can I use my own templates? Yes. Templates are [React Email](https://react.email) components in TSX, with typed variables validated at render time and a live preview in the dashboard editor. ```tsx theme={null} export function EmailTemplate({ user, variables }: Props) { return (

Hi {user.displayName}, check out {variables.featureName}!

); } ``` Hexclave also ships ready-made templates for the common flows - email verification, password reset, magic link, team invitations, and payment receipts - wired up automatically and customizable from the dashboard. ## Can I match my brand? Yes. Themes wrap every email in a consistent layout - header, footer, logo, background. Use the built-in Light, Dark, and Colorful themes, or write your own as a TSX component: ```tsx theme={null} import { ThemeProps, ProjectLogo } from "@hexclave/emails"; export function EmailTheme({ children, unsubscribeLink, projectLogos }: ThemeProps) { return ( <> {children} {unsubscribeLink && Unsubscribe} ); } ``` Set a project default, override per-email with `themeId`, or send with no theme at all. See [Templates & themes](./templates-and-themes) for the full theme API and built-in themes. ## Can I respect unsubscribes and preferences? Yes - and it's automatic. Every email is **Transactional** (always delivered) or **Marketing** (users can opt out). Mark the category when you send, and Hexclave skips users who've unsubscribed and appends an unsubscribe link to marketing mail for you. ```typescript theme={null} await hexclaveServerApp.sendEmail({ userIds: ["user-id"], subject: "Product updates", html: "

Check out what's new!

", notificationCategoryName: "Marketing", }); ``` ## Can I use my own email provider? Yes. Connect **custom SMTP**, plug in **Resend** with an API key, or let Hexclave run a **Managed** domain — you add the DNS records from onboarding; Hexclave handles signing and deliverability. Your built-in auth emails - verification, password resets, magic links - already work on Hexclave's **shared** development server out of the box; connect one of the custom providers when you're ready to send your own email and ship to production. Custom SMTP, Resend, and Managed can only be configured in the [cloud dashboard](https://app.hexclave.com). A [development environment](/guides/going-further/local-vs-cloud-dashboard) is limited to the shared server. ## Can I schedule and send in bulk? Yes. Pass `scheduledAt` to send later, and `allUsers: true` to reach your whole project. Delivery runs through an async pipeline that respects your sending capacity, so large sends don't tank your reputation. ```typescript theme={null} await hexclaveServerApp.sendEmail({ allUsers: true, templateId: "product-update", subject: "We just shipped a big update", scheduledAt: new Date("2027-01-01T00:00:00Z"), }); ``` ## Can I see what happened after I hit send? Yes. Every email's status is tracked - sent, bounced, marked as spam - across hourly, daily, weekly, and monthly windows, in the dashboard and from code. ```typescript theme={null} const info = await hexclaveServerApp.getEmailDeliveryStats(); // info.stats.day.sent, info.stats.day.bounced, ... ``` ## Can I compose without writing code? Yes. The dashboard has a full draft editor with live preview, theme selection, a recipient picker, and scheduling. You can then either send the draft from the dashboard or programmatically using the `draftId`. See [Drafts](./drafts). ## Start here 1. [Set up Hexclave](/guides/getting-started/setup), then enable **Emails** in the dashboard. 2. Connect an email server under **Emails → Email Settings** (SMTP, Resend, or Managed) - the shared server already powers your auth emails in development. 3. Call `hexclaveServerApp.sendEmail(...)` from your backend. Ready for a start-to-finish walkthrough — server setup, templates, sending, and delivery? Read the [Emails guide](./guide). # Templates & Themes Source: https://docs.hexclave.com/guides/apps/emails/templates-and-themes Build email bodies with React Email templates, then wrap them in reusable branded themes. Every email Hexclave sends is built from two pieces: * A **template** - your content (the body, subject, and notification category), written as a React Email component. * A **theme** - the shared layout and branding that wraps that content (header, footer, background, logo, unsubscribe link). Templates and themes are independent: one template renders consistently across any theme, and changing a theme restyles every email at once. Both ship with sensible built-ins and can be customized or replaced from the dashboard. ## Templates Templates are React Email components written in TSX. Each template receives the current `user`, `project`, and any custom `variables` you pass when sending. ```tsx theme={null} import { type } from "arktype"; import { Container } from "@react-email/components"; import { Subject, NotificationCategory, Props } from "@hexclave/emails"; export const variablesSchema = type({ featureName: "string", }); export function EmailTemplate({ user, project, variables, }: Props) { return (

Hi {user.displayName}, check out {variables.featureName}!

); } EmailTemplate.PreviewVariables = { featureName: "Dark mode", } satisfies typeof variablesSchema.infer; ``` Key concepts: * **`variablesSchema`** - Define the shape of your template variables using [arktype](https://arktype.io). Hexclave validates variables against this schema at render time. * **``** - Sets the email subject line from inside the template. * **``** - Declares whether this is a `"Transactional"` or `"Marketing"` email. * **`PreviewVariables`** - Sample data used for the live preview in the dashboard editor. ### Built-in templates Hexclave ships with templates for common auth and payment flows. These are sent automatically when the matching event happens: | Template | Trigger | | ---------------------- | ----------------------------------------------------------------------------------------------- | | **Email Verification** | User signs up or changes their email | | **Password Reset** | User requests a password reset | | **Magic Link/OTP** | User signs in with magic link or one-time password | | **Team Invitation** | User is invited to join a team | | **Sign In Invitation** | User is invited to create an account | | **Payment Receipt** | A payment succeeds (one-time or subscription) | | **Payment Failed** | A payment fails | | **Trial Ending Soon** | A [Payments free trial](/guides/apps/payments/products-and-pricing#free-trials) is about to end | You can customize any built-in template from the dashboard under **Emails → Templates**. ## Themes Themes wrap your email content in a consistent layout - header, footer, background, branding. Hexclave includes three built-in themes: * **Default Light** - Clean white background with subtle shadow * **Default Dark** - Dark background with light text * **Default Colorful** - Light purple background with an accent border You can create custom themes in the dashboard under **Emails → Email Settings → Themes**. Themes are also TSX components: ```tsx theme={null} import { Html, Head, Tailwind, Body, Container } from "@react-email/components"; import { ThemeProps, ProjectLogo } from "@hexclave/emails"; export function EmailTheme({ children, unsubscribeLink, projectLogos }: ThemeProps) { return ( {children} {unsubscribeLink && (

Unsubscribe

)}
); } ``` Set a default theme for your project in the dashboard. You can also override the theme per-email with the `themeId` option, or pass `themeId: false` to send without any theme. ## Related * [Drafts](./drafts) - compose in the dashboard and send with `draftId` (not TSX templates). * [Emails guide](./guide) - start-to-finish implementation: server, templates, sending, and delivery. # Launch Checklist Source: https://docs.hexclave.com/guides/apps/launch-checklist/overview Steps to prepare Hexclave for production use Hexclave makes development easy with various default settings, but these settings need to be optimized for security and user experience when moving to production. Here's a checklist of things you need to do before switching to production mode: ## Domains By default, Hexclave allows all localhost paths as valid callback URLs. This is convenient for development but poses a security risk in production because attackers could use their own domains as callback URLs to intercept sensitive information. Therefore, in production, Hexclave must know your domain (e.g., `https://your-website.com`) and only allow callbacks from those domains. Follow these steps when you're ready to push your application to production: Navigate to the `Domain & Handlers` tab in the Hexclave dashboard. If you haven't configured your handler, you can leave it as the default. (Learn more about handlers [here](/sdk/objects/hexclave-app)). For enhanced security, disable the `Allow all localhost callbacks for development` option. ## OAuth providers Hexclave uses shared OAuth keys for development to simplify setup when using "Sign in with Google/GitHub/etc." However, this isn't secure for production as it displays "Hexclave Development" on the providers' consent screens, making it unclear to users if the OAuth request is genuinely from your site. Thus, you should configure your own OAuth keys with the providers and connect them to Hexclave. To use your own OAuth provider setups in production, follow these steps for each provider you use: On the provider's website, create an OAuth app and set the callback URL to the corresponding Hexclave callback URL. Copy the client ID and client secret. [Google OAuth Setup Guide](https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name-.) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/google ``` [GitHub OAuth Setup Guide](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/github ``` [Facebook OAuth Setup Guide](https://developers.facebook.com/docs/development/create-an-app/facebook-login-use-case) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/facebook ``` [Microsoft Azure OAuth Setup Guide](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/microsoft ``` [Spotify OAuth Setup Guide](https://developer.spotify.com/documentation/general/guides/app-settings/) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/spotify ``` [Gitlab OAuth Setup Guide](https://docs.gitlab.com/ee/integration/oauth_provider.html) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/gitlab ``` [Bitbucket OAuth Setup Guide](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/bitbucket ``` [LinkedIn OAuth Setup Guide](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext\&tabs=HTTPS1) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/linkedin ``` [X OAuth Setup Guide](https://developer.x.com/en/docs/apps/overview) Callback URL: ``` https://api.hexclave.com/api/v1/auth/oauth/callback/x ``` Go to the `Auth Methods` section in the Hexclave dashboard, open the provider's settings, switch from shared keys to custom keys, and enter the client ID and client secret. ## Email server For development, Hexclave uses a shared email server, which sends emails from Hexclave's domain. This is not ideal for production as users may not trust emails from an unfamiliar domain. You should set up an email server connected to your own domain. Steps to connect your own email server with Hexclave: 1. **Setup Email Server**: Configure your own email server and connect it to your domain (this step is beyond Hexclave's documentation scope). 2. **Configure Hexclave's Email Settings**: Navigate to the `Emails` section in the Hexclave dashboard, click `Edit` in the `Email Server` section, switch from `Shared` to `Custom SMTP server`, enter your SMTP configurations, and save. ## Enabling production mode After completing the steps above, you can enable production mode on the `Project Settings` tab in the Hexclave dashboard, ensuring that your website runs securely with Hexclave in a production environment. # Billing & Invoices Source: https://docs.hexclave.com/guides/apps/payments/billing-and-invoices Save payment methods and display a customer's billing history ## Payment methods Customers can save a payment method for future purchases and plan switches. This is built on a secure setup-intent flow. ```typescript theme={null} // Create a setup intent const setupIntent = await user.createPaymentMethodSetupIntent(); // setupIntent.clientSecret - use with the embedded card form to collect card details // After the user completes the card form: const paymentMethod = await user.setDefaultPaymentMethodFromSetupIntent( setupIntentId ); // paymentMethod contains: id, brand, last4, exp_month, exp_year // (it may be null, and the card fields can individually be null) ``` To check if a customer has a payment method saved: ```typescript theme={null} // Client component (hook) const billing = user.useBilling(); // Server component const billing = await user.getBilling(); // billing.hasCustomer - whether a billing customer exists // billing.defaultPaymentMethod - card details or null ``` ## Invoices List a customer's invoices for displaying billing history: ```typescript theme={null} // Client component (hook) const invoices = user.useInvoices(); // Server component const invoices = await user.listInvoices(); ``` Each invoice includes `createdAt`, `status` (`"draft"`, `"open"`, `"paid"`, `"uncollectible"`, `"void"`, or `null`), `amountTotal` (in cents), and `hostedInvoiceUrl` (a link to the hosted invoice page, or `null`). Invoices support pagination: ```typescript theme={null} const firstPage = await user.listInvoices({ limit: 10 }); const secondPage = await user.listInvoices({ limit: 10, cursor: firstPage.nextCursor, }); ``` Billing and invoices are available for user and team customers only, not custom customers (see [Customer Types](./customers)). To reverse a charge, see [Refunds](./refunds). Refunds are issued from **Payments -> Transactions**, not from the customer invoice list. ## Related * [Subscriptions](./subscriptions) - switching plans requires a saved payment method * [Checkout & Purchases](./checkout) - sell a product * [Refunds](./refunds) - reverse a charge and optionally end access # Checkout & Purchases Source: https://docs.hexclave.com/guides/apps/payments/checkout Sell a product with a checkout URL and read what a customer owns To sell a product, generate a checkout URL and redirect the customer to it. Hexclave runs the hosted checkout, receives the payment confirmation, and grants the product - you never write a webhook handler. ## Selling a product The `createCheckoutUrl` method is available on both user and team objects. ```typescript title="app/components/purchase-button.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function PurchaseButton({ productId }: { productId: string }) { const user = useUser({ or: 'redirect' }); const handlePurchase = async () => { const checkoutUrl = await user.createCheckoutUrl({ productId, returnUrl: window.location.href, }); window.location.href = checkoutUrl; }; return ; } ``` ```typescript title="app/purchase/page.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function PurchasePage() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const checkoutUrl = await user.createCheckoutUrl({ productId: "prod_premium_monthly", }); return Upgrade to Premium; } ``` For team purchases, call `createCheckoutUrl` on the team object instead: ```typescript theme={null} const team = user.useTeam(teamId); const checkoutUrl = await team.createCheckoutUrl({ productId }); ``` If you're using a non-JS backend (Python, Go, etc.), call the REST API directly: `POST /api/v1/payments/purchases/create-purchase-url` with `customer_type`, `customer_id`, and `product_id`. See the [REST API overview](/api/overview) for details. On the **server**, you can also pass an inline product definition instead of `productId`, or create a URL for a [custom customer](./customers): ```typescript theme={null} import { hexclaveServerApp } from "@/hexclave/server"; const checkoutUrl = await hexclaveServerApp.createCheckoutUrl({ customCustomerId: "external-org-123", productId: "prod_enterprise", returnUrl: "https://example.com/billing", }); ``` ## Hosted checkout `createCheckoutUrl` returns a hosted Hexclave page (`/purchase/{code}`). Price and quantity are chosen **on that page**, not in the URL. Stackable products show a quantity selector. Checkout URLs expire after **24 hours**. If **Block new purchases** is on in **Payments -> Settings**, creating a URL and completing checkout both fail; existing subscriptions keep renewing. A \*\*$0 recurring** price activates without collecting a card. One-time $0 prices cannot go through checkout. [Free trials](./products-and-pricing#free-trials) collect a card up front and charge it when the trial ends. In **test mode**, checkout grants the product immediately and skips the trial. ## Creating a checkout URL from the dashboard You don't have to generate URLs in code. **Create checkout** is available from: * **Payments -> Customers** (user, team, or custom) * A product's detail page, and product cards in **Payments -> Product Lines** * The **Users** and **Teams** tables The URL still expires in 24 hours. Send it to the customer, or open it yourself while testing. ## Checking what a customer owns After a purchase, you'll want to know what the customer has. Check their product list, or check a specific [item balance](./items-and-entitlements). ```typescript theme={null} // Client component (hook - re-renders on changes) const products = user.useProducts(); // Server component const products = await user.listProducts(); ``` Each product in the list includes: * `id` - The product ID (or `null` for inline products) * `displayName` - The product name * `quantity` - How many the customer owns (relevant for stackable products) * `subscription` - `null` for one-time products, or an object with `subscriptionId`, `currentPeriodEnd`, `cancelAtPeriodEnd`, and `isCancelable` for subscriptions * `switchOptions` - Other products in the same product line the customer could switch to ## Related * [Items & Entitlements](./items-and-entitlements) - read and consume credits, seats, and quota * [Subscriptions](./subscriptions) - manage recurring plans after purchase * [Refunds](./refunds) - reverse a purchase from the dashboard * [Granting Products](./granting-products) - give access without checkout # Customer Types Source: https://docs.hexclave.com/guides/apps/payments/customers Bill users, teams, and custom external customers A **customer** is whoever owns a purchase. Hexclave supports three types: * **Users** - Individual user accounts. Users can manage their own purchases, billing, and invoices from the client SDK. * **Teams** - Team or organization accounts. Team admins can create checkouts, switch plans, and cancel subscriptions for their team. * **Custom customers** - External entities identified by an arbitrary string ID. Useful for integrations with external systems. They do not support billing, invoices, or plan switching; checkout and grants go through the server SDK or the dashboard. ## Where each method lives The full billing surface - [`createCheckoutUrl`](./checkout), [`useProducts`](./checkout#checking-what-a-customer-owns), [`useItem`](./items-and-entitlements), [`switchSubscription`](./subscriptions), [`useBilling` and `useInvoices`](./billing-and-invoices) - is available on both **user** and **team** objects. For **custom customers**, use the top-level `hexclaveServerApp` methods with `customCustomerId` (for example [`createCheckoutUrl`](./checkout), [`grantProduct`](./granting-products), `getItem`, and `listProducts`). Billing, invoices, payment methods, and subscription switching are not available for custom customers. Every product declares a **customer type** when you [define it](./products-and-pricing#defining-products), which determines who can purchase it. From the dashboard, **Payments -> Customers** lists users, teams, and custom customers (custom IDs appear after they have transactions). You can adjust item balances and [create a checkout URL](./checkout#creating-a-checkout-url-from-the-dashboard) for any of them. Granting a product without checkout is [SDK/API only](./granting-products). # Granting Products Source: https://docs.hexclave.com/guides/apps/payments/granting-products Give a customer a product without going through checkout Sometimes you need to give a customer a product without charging them - comps, promotional offers, or support access. Use `grantProduct` on the server. There is **no grant-product button** in the dashboard — from the dashboard, use [Create checkout](./checkout#creating-a-checkout-url-from-the-dashboard) (even in test mode) or adjust item quantities. A grant creates a real subscription or one-time product on the customer. It does **not** honor a configured [free trial](./products-and-pricing#free-trials) — access starts immediately. Pass `quantity` for stackable products (defaults to `1`). ## Granting a configured product ```typescript theme={null} import { hexclaveServerApp } from "@/hexclave/server"; // Grant a pre-configured product to a user await hexclaveServerApp.grantProduct({ userId: "user-id", productId: "prod_premium", quantity: 1, }); // Grant to a team await hexclaveServerApp.grantProduct({ teamId: "team-id", productId: "prod_team_plan", }); // Grant to a custom customer await hexclaveServerApp.grantProduct({ customCustomerId: "external-org-123", productId: "prod_enterprise", }); ``` If you already have a reference to a **server** user or team object (for example from `hexclaveServerApp.getUser(userId)`), you can also call `user.grantProduct({ productId, quantity })` directly. This method is server-only. ## Inline products You can also grant products with an **inline definition** - no pre-configured product needed. This is useful for one-off grants like bonus credits: ```typescript theme={null} import { hexclaveServerApp } from "@/hexclave/server"; await hexclaveServerApp.grantProduct({ userId: "user-id", product: { display_name: "Bonus Credits", customer_type: "user", server_only: true, stackable: false, prices: { manual: { USD: "0" }, }, included_items: { credits: { quantity: 100 }, }, }, }); ``` ## Related * [Items & Entitlements](./items-and-entitlements) - the credits and seats a grant hands out * [Checkout & Purchases](./checkout) - charge the customer instead * [Customer Types](./customers) - grant to users, teams, or custom customers # Items & Entitlements Source: https://docs.hexclave.com/guides/apps/payments/items-and-entitlements Track and consume credits, seats, and API quota with race-safe item balances Items are the building blocks of entitlements. Instead of just recording that a customer "bought the Pro plan," Hexclave tracks the quantifiable things that plan grants - **credits**, **seats**, **API calls** - as item balances on the customer, and keeps them in sync as customers buy, consume, renew, and churn. When a product includes items like "100 credits" or "5 seats", those quantities are granted on purchase. Each included item is configured with: * **Quantity** - How much to grant * **Repeat** - An optional refresh interval (e.g. grant again every month), or `never` * **Expires** - When the grant expires: `never`, `when-purchase-expires`, or `when-repeated` ## Checking item balances ```typescript theme={null} // Client component (hook - re-renders on changes) const credits = user.useItem("credits"); // Server component const credits = await user.getItem("credits"); ``` An item has two quantity fields: * `quantity` - The raw balance (can be negative if you've consumed more than granted) * `nonNegativeQuantity` - `Math.max(0, quantity)` for display purposes Here's a practical example - showing a credits counter: ```typescript title="app/components/credits-widget.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function CreditsWidget() { const user = useUser({ or: 'redirect' }); const credits = user.useItem("credits"); return (

Available Credits

{credits.nonNegativeQuantity}

); } ``` ## Consuming credits (server-side) When your app needs to consume credits (e.g. when a user sends an AI request), use `tryDecreaseQuantity` on the server. It's a single transactional operation - it returns `false` and does nothing if the balance would go negative, so concurrent requests can't overspend. ```typescript title="lib/credits.ts" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export async function consumeCredits(userId: string, amount: number) { const user = await hexclaveServerApp.getUser(userId); if (!user) throw new Error("User not found"); const credits = await user.getItem("credits"); const success = await credits.tryDecreaseQuantity(amount); if (!success) { throw new Error("Insufficient credits"); } return { remaining: credits.quantity }; } ``` Always use `tryDecreaseQuantity()` instead of checking the balance and then decreasing. This prevents race conditions where multiple requests could consume more credits than available. You can also increase a balance with `credits.increaseQuantity(amount)` or decrease without the safety check using `credits.decreaseQuantity(amount)`. These consumption methods are **server-only** - client-side `useItem` returns a read-only balance. ## Adjusting balances from the dashboard In **Payments -> Customers** you can view item balances per customer and manually adjust quantities (plus or minus, with an optional description) - useful for support credits, comps, or corrections. The dashboard dialog does not set an expiration on those adjustments. ## Related * [Products & Pricing](./products-and-pricing) - attach items to a product * [Granting Products](./granting-products) - grant a one-off bundle of items # Payments Source: https://docs.hexclave.com/guides/apps/payments/overview Sell subscriptions and one-time products, and manage entitlements like credits and seats - billing without the plumbing **For agents/LLMs:** This is a high-level *marketing* overview of the Payments app, not an implementation reference. To actually build with payments, start at [Payments setup](./setup), then use the concept pages: [Products & Pricing](./products-and-pricing), [Items & Entitlements](./items-and-entitlements), [Checkout & Purchases](./checkout), [Subscriptions](./subscriptions), [Billing & Invoices](./billing-and-invoices), [Refunds](./refunds), [Granting Products](./granting-products), and [Customer Types](./customers). Charging the card is the easy part. Everything *after* that - turning a payment into access, keeping it in sync, handling upgrades, refunds, and renewals - is the part you'd normally build and maintain yourself. The Payments app owns that layer. You define products in the dashboard; Hexclave runs checkout, grants and revokes entitlements, tracks subscriptions, and keeps your billing state correct. Below are the questions developers actually ask, and the honest answers. ## Can I sell a subscription or one-time product? Yes. Define products and prices in the dashboard, then generate a checkout URL and redirect. Hexclave handles the checkout session, the webhook, and granting access on success - you never write a webhook handler. ```typescript theme={null} const checkoutUrl = await user.createCheckoutUrl({ productId: "pro_monthly", returnUrl: window.location.href, }); window.location.href = checkoutUrl; ``` Group products into a **product line** to make them mutually exclusive - Free, Pro, and Enterprise where a customer can only hold one at a time. Upgrades, downgrades, and proration on plan switches are handled for you. Recurring prices can include a **free trial** — the card is collected at checkout and charged when the trial ends. See [Products & Pricing](./products-and-pricing#free-trials). ## Can I sell credits, seats, or API quota? Yes - and this is the real point. Products include **items**: quantifiable entitlements like credits, seats, or API calls, with their own quantity, refresh schedule, and expiration. When a customer buys, the items are granted. When their plan ends, they're revoked. You don't track any of it in your own database. Read a balance anywhere with a hook that stays in sync, and check access without a billing round-trip: ```typescript theme={null} // Client - re-renders when the balance changes const credits = user.useItem("credits"); return {credits.nonNegativeQuantity} credits left; ``` ## Can I consume credits safely under load? Yes. Consume on the server with `tryDecreaseQuantity` - a single transactional operation that returns `false` instead of letting a balance go negative. No read-then-write race, no double-spend when two requests land at once. ```typescript theme={null} const credits = await user.getItem("credits"); const ok = await credits.tryDecreaseQuantity(1); if (!ok) throw new Error("Out of credits"); // ...do the work ``` Grant more with `increaseQuantity`, or adjust manually from the dashboard. This is the consumption layer you'd otherwise build by hand with a ledger table and a pile of webhooks. ## What exactly does Hexclave own for me? With a raw payment processor you build and maintain the glue: webhook handlers, granting and revoking access on payment, proration on upgrades, refunds, subscription endings, and keeping your own database in sync with all of it. Hexclave owns that layer: * **Webhooks** - received and reconciled for you; there's nothing to host * **Grants & revokes** - products and items are applied on payment and removed when a subscription ends or a refund is issued * **Subscription lifecycle** - status, renewals, cancellations, and period ends tracked automatically * **Proration** - handled automatically on plan switches, triggered through one SDK call * **Refunds** - issued from the dashboard (partial or full), with optional entitlement revocation. See [Refunds](./refunds). * **Sync** - entitlement and subscription state stays consistent without a database to babysit ## Can I bill teams and external customers, not just users? Yes. Every customer is a **user**, a **team**, or a **custom customer** (any external entity you key by ID). The billing surface - `createCheckoutUrl`, `useProducts`, `useItem`, `switchSubscription` - lives on user and team objects; custom customers are managed from the server SDK for items, products, and grants. ## Can I manage subscriptions in code? Yes. Switch a customer between plans in the same product line, or cancel, with a single call - proration and timing handled for you. ```typescript theme={null} await user.switchSubscription({ fromProductId: "pro_monthly", toProductId: "enterprise_monthly", }); await hexclaveServerApp.cancelSubscription({ productId: "pro_monthly" }); ``` You can also `grantProduct` directly - for trials, comps, or admin grants - with a pre-defined product or an inline one-off definition. ## Can I build and test before going live? Yes. Flip on **test mode** and purchases are granted instantly for free - no real charge, no live round-trip - so you can wire up entitlements end to end before connecting a real account. ## What it costs, and what it doesn't do Straight answers so there are no surprises: * **Platform fee** - Hexclave takes a **0.9%** fee on charges, on top of standard payment processing fees. * **Currency** - payments are processed in **USD** today. * **Not a marketplace** - Payments run through a single connected payment account per project. It isn't built for marketplace-style payouts to many sellers. * **Not locked in** - this is a convenience layer, not a cage. Use it because it saves you the billing plumbing, not because you have to. ## Start here 1. [Set up Hexclave](/guides/getting-started/setup), then enable **Payments** in the dashboard. 2. Connect your payment account under **Payments → Settings**, and turn on **test mode** while you build. 3. Define a product (with items, if you want entitlements), then call `user.createCheckoutUrl(...)`. Ready for the details - products, items, subscriptions, billing, invoices, refunds, and grants? Start with [Payments setup](./setup). # Products & Pricing Source: https://docs.hexclave.com/guides/apps/payments/products-and-pricing Define what you sell - products, prices, product lines, add-ons, and trials A **product** is anything a customer can buy: a subscription plan, a one-time purchase, or a credit pack. Configure your products in **Payments -> Products & Items**, or in `hexclave.config.ts` under `payments.products`. ## Defining products Each product has: * **Display name** - What the customer sees * **Customer type** - Whether this product is for users, teams, or custom customers (see [Customer Types](./customers)) * **Prices** - One or more prices, each with a currency amount and an optional billing interval (day, week, month, or year). Amounts are **decimal strings** like `"9.99"` or `"1000"` — not cent integers. Live charges are processed in **USD**. * **Included items** - Items granted when the product is purchased, with configurable quantity, repeat schedule, and expiration behavior (see [Items & Entitlements](./items-and-entitlements)) A few additional options: * **Free trial** - Give customers a trial period before charging. Prefer setting this **on the price**; a product-level trial is a fallback. See [Free trials](#free-trials). * **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one. Every add-on base must live in the **same product line**. * **Server-only** - Hide the product (or an individual price) from client SDK responses. Useful for products that should only be granted programmatically. * **Stackable** - Allow multiple purchases of the same product (default is one per customer). Quantity is chosen on the [hosted checkout](./checkout#hosted-checkout) page. A recurring price can be \*\*$0** (a free plan in a product line). One-time $0 prices are not sold through checkout — use [grantProduct](./granting-products) or test mode instead. ## Product lines A **product line** groups products that are mutually exclusive. For example, a "Plan" product line might contain Free, Pro, and Enterprise - a customer can only hold one at a time. When they upgrade or downgrade, the old product is replaced. Assign products to a product line to power plan tiers and enable [subscription switching](./subscriptions). Configure lines in **Payments -> Product Lines**. Add-ons are exempt from product-line exclusivity - a customer can own an add-on alongside their base product in the same line. ## Free trials A free trial defers the first charge on a **recurring** price. Checkout still collects a card; it is not charged until the trial ends, and the billing cycle starts from that date. Customers get a **Trial Ending Soon** email beforehand. Configure the trial **on the price** in the price editor. A product-level trial still works as a fallback, but the price-level value wins when both are set. Trials cannot be combined with: * One-time (non-recurring) prices * \$0 recurring prices * Durations longer than **730 days** (a processor limit) They also **do not run in test mode** — the product is granted immediately with no trial. [Plan switches](./subscriptions) do not start a new trial on the destination product. [grantProduct](./granting-products) also skips trials and creates an active subscription. ## Related * [Items & Entitlements](./items-and-entitlements) - attach credits, seats, and quota to a product * [Checkout & Purchases](./checkout) - sell a product once it's defined * [Subscriptions](./subscriptions) - switching plans after a trial or paid period # Refunds Source: https://docs.hexclave.com/guides/apps/payments/refunds Reverse a charge from the dashboard, and optionally end the customer's access Refunds are issued from **Payments -> Transactions**. Open a **purchase** row and choose **Refund**. You can return money, end the product, or both. ## Dashboard refunds Each refund has two knobs: 1. **Amount (USD)** - Partial or full. Leave at `0` to change access without returning money (for example after a prior partial refund, or for a test-mode purchase). 2. **Product / subscription lifecycle** * **End now** - Revoke the product immediately and expire item grants that were tied to the purchase (`when-purchase-expires` or `when-repeated`). Permanent grants (`expires: never`) stay. * **End at period end** - Subscriptions only. Access continues until the current period ends. * **No change** - Refund money only; the customer keeps the product. You must set a non-zero amount **or** change the lifecycle. Multiple partial refunds are allowed until the original charge is fully returned. The dashboard refunds the **original purchase**, not later **subscription-renewal** rows. One-time purchases only support **End now** (they have no period). ### Test mode Test-mode purchases never charged a card, so money refunds are disabled. You can still choose **End now** to revoke access. ### Platform fee Hexclave's **0.9%** platform fee is not returned when you refund a charge. Processor fees follow the processor's own refund rules. Subscriptions created with [`grantProduct`](./granting-products) have no processor charge, so a refund can only change lifecycle — not return money. ## Related * [Billing & Invoices](./billing-and-invoices) - invoices and saved payment methods * [Subscriptions](./subscriptions) - cancel without a refund * [Items & Entitlements](./items-and-entitlements) - what happens to credits when access ends # Setup Source: https://docs.hexclave.com/guides/apps/payments/setup Enable Payments, connect a payment account, and understand how the pieces fit together Hexclave includes a Payments app that handles billing, subscriptions, and one-time purchases. Instead of building your own billing system, you define products in the dashboard and Hexclave takes care of checkout, entitlement tracking, subscription lifecycle, and invoicing. ## Getting started Go to the **Apps** section in your dashboard, find **Payments**, and enable it. Open **Payments -> Settings** and follow the onboarding flow. You'll be asked for business details, bank info, and identity verification. Once approved, payments are live. While building, enable **test mode** in **Payments -> Settings**. All purchases will be free - no real money is charged. You can switch to live when you're ready. Hexclave Payments is currently only available for US-based businesses, and processes payments in **USD**. Support for other countries and currencies is coming soon. ## Core concepts Before writing any code, it helps to understand how the pieces fit together. A **product** is something you sell - a subscription plan, a one-time purchase, or a credit pack. Products can have one or more **prices** (one-time or recurring), and they can include **items** - quantifiable entitlements like credits, seats, or API calls. A **product line** groups products that are mutually exclusive. For example, a "Plan" product line might contain Free, Pro, and Enterprise - a customer can only have one at a time. When they upgrade, the old plan is replaced. A **customer** is whoever owns the purchase. This can be a user, a team, or a custom external entity. Here's how these pieces look in practice: And the typical flow to make it all work: 1. You define products and items in the dashboard 2. Your app generates a checkout URL and redirects the user to the hosted checkout page 3. The user pays, Hexclave is notified, and the product is granted 4. Your app reads the customer's products and item balances to control access Each step has its own guide: * [Products & Pricing](./products-and-pricing) - define what you sell * [Items & Entitlements](./items-and-entitlements) - credits, seats, and quota * [Checkout & Purchases](./checkout) - sell a product and read what a customer owns * [Subscriptions](./subscriptions) - switching plans and cancellation * [Billing & Invoices](./billing-and-invoices) - payment methods and billing history * [Refunds](./refunds) - partial or full refunds, and ending access * [Granting Products](./granting-products) - give access without checkout * [Customer Types](./customers) - users, teams, and custom customers ## Test mode While you're building, turn on **test mode** in **Payments -> Settings**. With test mode on, purchases skip the payment processor entirely: products and items are granted instantly, no card is collected, and no money moves. It's on by default in development environments. This lets you wire up checkout, entitlements, and subscription logic end to end without touching real money. When you're confident everything works, turn test mode off - at which point purchases go through the payment processor and **charge real money**. Test mode does **not** simulate every live behavior: * **Free trials are skipped.** The product is granted immediately; there is no trial period and no deferred first charge. Turn test mode off to exercise the real trial checkout. * **Catalog edits still apply to production.** Test mode only changes how purchases run, not which products exist. * **Money refunds are not available.** You can still end access from a refund dialog; you cannot return money that was never charged. See [Refunds](./refunds). There's no "live but free" middle ground. With test mode **off**, every purchase is a real, billable charge. Keep test mode on until you're ready to take real payments. ## Dashboard The dashboard gives you full visibility and control over your payments: * **Product Lines** - Group products into mutually exclusive tiers. In **Payments -> Product Lines**. * **Products & Items** - Create and edit products, set pricing, and configure included items. In **Payments -> Products & Items**. * **Customers** - View item balances per customer, manually adjust quantities, and create checkout URLs. In **Payments -> Customers**. Granting a product without checkout is [SDK/API only](./granting-products). * **Transactions** - See all payment activity, filter by type and customer, export CSV, and issue [refunds](./refunds) on purchase rows. In **Payments -> Transactions**. * **Payouts** - View payout information. In **Payments -> Payouts**. Unavailable in development environments. * **Settings** - Connect your payment account, toggle test mode, configure payment methods, and **block new purchases** (existing subscriptions keep renewing). In **Payments -> Settings**. ### Payment emails Email notifications are sent automatically on payment events: * **Payment Receipt** - Sent on successful payment with product details, amount, and receipt link * **Payment Failed** - Sent on failed payment with product name, amount, and failure reason * **Trial Ending Soon** - Sent before a [free trial](./products-and-pricing#free-trials) ends, so the customer knows the saved payment method will be charged These apply to both one-time purchases and subscription renewals (receipts and failures). Customize them in **Emails -> Templates** (see the [Emails guide](/guides/apps/emails/guide)). # Subscriptions Source: https://docs.hexclave.com/guides/apps/payments/subscriptions Switch plans with automatic proration, and cancel subscriptions When products belong to the same [product line](./products-and-pricing#product-lines), customers can move between them, and Hexclave handles the proration and timing for you. ## Switching plans For example, upgrading from Pro to Enterprise: ```typescript title="app/components/upgrade-button.tsx" theme={null} "use client"; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function UpgradeButton() { const user = useUser({ or: 'redirect' }); return ( ); } ``` Switching plans requires the customer to have a default payment method saved (see [Billing & Invoices](./billing-and-invoices)). The switch happens immediately and the charge is prorated automatically. You cannot switch to an add-on, and a [free trial](./products-and-pricing#free-trials) on the destination product is not applied. ## Canceling a subscription `cancelSubscription` is called on the app instance rather than the user object: ```typescript theme={null} "use client"; import { useHexclaveApp } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function CancelButton({ productId }: { productId: string }) { const app = useHexclaveApp(); return ( ); } ``` ```typescript theme={null} import { hexclaveServerApp } from "@/hexclave/server"; // Cancel for the current user await hexclaveServerApp.cancelSubscription({ productId: "prod_pro" }); // Cancel for a team await hexclaveServerApp.cancelSubscription({ productId: "prod_team_plan", teamId: "team-id", }); ``` `cancelSubscription` is **immediate** for live, processor-backed subscriptions — access ends now. Subscriptions created in **test mode** or via [grantProduct](./granting-products) (no processor subscription) are marked to end at the current period end, so access continues until then. Pass `subscriptionId` when canceling an [inline product](./granting-products#inline-products) that has no catalog `productId`. One-time purchases cannot be canceled; [refund](./refunds) them instead. ## Reading subscription state Each subscription product returned by [`useProducts` / `listProducts`](./checkout#checking-what-a-customer-owns) carries a `subscription` field with the current period end and cancellation state, plus `switchOptions` listing the other products in the same product line the customer can switch to. ## Related * [Checkout & Purchases](./checkout) - start a subscription * [Billing & Invoices](./billing-and-invoices) - payment methods and billing history * [Refunds](./refunds) - reverse a charge and optionally end the subscription # RBAC Permissions Source: https://docs.hexclave.com/guides/apps/rbac/overview Control what each user can do and access within your application Permissions are a way to control what each user can do and access within your application. Hexclave RBAC lets you define reusable permission IDs in the dashboard, compose them into higher-level roles, assign team permissions to team members, and check permissions from the SDK. ## Permission Types Hexclave supports two types of permissions: 1. **Team Permissions**: Control what a user can do within a specific team 2. **Project Permissions**: Control what a user can do globally, across the entire project Both permission types can be managed from the dashboard, and both support arbitrary nesting. ## Dashboard The RBAC app adds two dashboard pages: * **Project Permissions** - Global permissions that apply outside of a team context. The dashboard page defines the permissions and their hierarchy. * **Team Permissions** - Permissions scoped to a team. The dashboard page defines the permissions and their hierarchy, and team-member assignment happens from the Teams app. The RBAC pages define permission definitions. Team permission assignment is available from the Teams member table; project permission grants and revokes are done from server-side SDK code. ### Permission table Both pages use the same permission table: * **ID** - The permission ID used in SDK calls, such as `access_admin_dashboard` or `team:billing:manage`. * **Description** - Optional human-readable context for the permission. * **Contained Permissions** - Directly contained permissions, shown as badges. This column intentionally shows only direct children, not the full recursive expansion. * **Actions** - Edit and delete actions for custom permissions. The table has a **Filter** search box, infinite loading for larger team-permission sets, and URL-synced table state so filtered views can be shared or reloaded. ### Creating a permission Click **Create Permission** from either RBAC page. The dialog contains: * **ID** - Required, unique across project and team permission definitions. IDs may contain lowercase letters, numbers, `_`, and `:` only. * **Description** - Optional text shown in the dashboard table. * **Contained Permission IDs** - A checklist of permissions of the same type. For example, a team permission can contain other team permissions, and a project permission can contain other project permissions. Contained permissions are recursive. If `admin` contains `moderator`, and `moderator` contains `read`, then a user with `admin` also has `read`. ### Editing a permission Use the row action menu and choose **Edit**. The edit dialog keeps the same fields, with one important difference: **ID** is disabled. To rename a permission, create a new permission and migrate your checks/assignments. The contained-permissions checklist shows inherited permissions with a `from ` note, so you can tell whether a permission is selected directly or included through another selected permission. ### Deleting a permission Use the row action menu and choose **Delete**. Deleting is destructive and requires confirming: ```text theme={null} I understand this will remove the permission from all users and other permissions that contain it. ``` Deleting a permission removes the definition, removes it from users who had it directly, and removes it from other permissions that contained it. ### System permissions Hexclave comes with predefined team permissions known as system permissions. These IDs start with `$`. System permissions: * Can be assigned to members * Can be included inside custom permissions * Cannot be edited or deleted from the dashboard The permission table marks system permissions with an info tooltip, and hides the edit/delete action menu for those rows. ### Assigning team permissions Team permission definitions are created in **RBAC -> Team Permissions**, but assignments happen from the Teams app: 1. Open **Teams**. 2. Select a team. 3. Open the members table. 4. Use the row action menu for a member and choose **Edit permissions**. The member permissions dialog shows the same nested permission checklist. The members table's **Permissions** column shows only direct permissions for each user. If the permission lookup fails, the row shows **Failed to load** and the edit action is disabled until the table is reloaded. ## Team Permissions Team permissions control what a user can do within each team. You can create and assign permissions to team members from the Hexclave dashboard. These permissions could include actions like `create_post` or `read_secret_info`, or roles like `admin` or `moderator`. Within your app, you can verify if a user has a specific permission within a team. Permissions can be nested to create a hierarchical structure. For example, an `admin` permission can include both `moderator` and `user` permissions. We provide tools to help you verify whether a user has a permission directly or indirectly. ### Creating a Permission To create a new permission, navigate to **RBAC -> Team Permissions** in the Hexclave dashboard. Click **Create Permission**, set the permission ID, optionally add a description, and choose any contained permissions. Any permissions included within these selected permissions will also be recursively included. ### System Permissions Hexclave comes with a few predefined team permissions known as system permissions. These permissions start with a dollar sign (`$`). While you can assign these permissions to members or include them within other permissions, you cannot modify them as they are integral to the Hexclave backend system. ### Checking if a User has a Permission To check whether a user has a specific permission within a team, use `hasPermission`, `getPermission`, or the `usePermission` hook on the `User` object. `getPermission` returns the `Permission` object if the user has it; otherwise, it returns `null`. Always perform permission checks on the server side for business logic, as client-side checks can be bypassed. Here's an example: ```tsx title="Check user permission on the client" theme={null} "use client"; import { useUser } from "@hexclave/next"; export function CheckUserPermission() { const user = useUser({ or: 'redirect' }); const team = user.useTeam('some-team-id'); const permission = user.usePermission(team, 'read'); // Don't rely on client-side permission checks for business logic. return (
{permission ? 'You have the read permission' : 'You shall not pass'}
); } ```
```tsx title="Check user permission on the server" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CheckUserPermission() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const team = await hexclaveServerApp.getTeam('some-team-id'); const permission = await user.getPermission(team, 'read'); // This is a server-side check, so it's secure. return (
{permission ? 'You have the read permission' : 'You shall not pass'}
); } ```
For authorization logic, prefer a boolean server-side check: ```tsx title="app/api/team-settings/route.ts" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export async function POST() { const user = await hexclaveServerApp.getUser({ or: "throw" }); const team = await hexclaveServerApp.getTeam("some-team-id"); if (!team || !(await user.hasPermission(team, "team:settings:update"))) { return new Response("Forbidden", { status: 403 }); } // Update team settings here. return new Response("OK"); } ``` ### Listing All Permissions of a User To get a list of all permissions a user has in a team, use the `listPermissions` method or the `usePermissions` hook on the `User` object. By default, the list includes direct and indirect permissions. Pass `{ recursive: false }` if you only want direct assignments. Here is an example: ```tsx title="List user permissions on the client" theme={null} "use client"; import { useUser } from "@hexclave/next"; export function DisplayUserPermissions() { const user = useUser({ or: 'redirect' }); const team = user.useTeam('some-team-id'); const permissions = user.usePermissions(team); return (
{permissions.map(permission => (
{permission.id}
))}
); } ```
```tsx title="List user permissions on the server" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function DisplayUserPermissions() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const team = await hexclaveServerApp.getTeam('some-team-id'); const permissions = team ? await user.listPermissions(team) : []; return (
{permissions.map(permission => (
{permission.id}
))}
); } ```
### Granting a Permission to a User To grant a permission to a user, use the `grantPermission` method on the `ServerUser`. Here's an example: ```tsx theme={null} const team = await hexclaveServerApp.getTeam('teamId'); const user = await hexclaveServerApp.getUser(); if (!team || !user) throw new Error("Team or user not found"); await user.grantPermission(team, 'read'); ``` ### Revoking a Permission from a User To revoke a permission from a user, use the `revokePermission` method on the `ServerUser`. Here's an example: ```tsx theme={null} const team = await hexclaveServerApp.getTeam('teamId'); const user = await hexclaveServerApp.getUser(); if (!team || !user) throw new Error("Team or user not found"); await user.revokePermission(team, 'read'); ``` ## Project Permissions Project permissions are global permissions that apply to a user across the entire project, regardless of team context. These permissions are useful for handling things like premium plan subscriptions or global admin access. ### Creating a Project Permission To create a new project permission, navigate to **RBAC -> Project Permissions** in the Hexclave dashboard. Similar to team permissions, you can set an ID, add a description, and select other project permissions that the new permission contains. ### Checking if a User has a Project Permission To check whether a user has a specific project permission, use `hasPermission`, `getPermission`, or the `usePermission` hook. Here's an example: ```tsx title="Check user permission on the client" theme={null} "use client"; import { useUser } from "@hexclave/next"; export function CheckGlobalPermission() { const user = useUser({ or: 'redirect' }); const permission = user.usePermission('access_admin_dashboard'); return (
{permission ? 'You can access the admin dashboard' : 'Access denied'}
); } ```
```tsx title="Check user permission on the server" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function CheckGlobalPermission() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const permission = await user.getPermission('access_admin_dashboard'); return (
{permission ? 'You can access the admin dashboard' : 'Access denied'}
); } ```
For authorization logic, prefer a server-side boolean check: ```tsx title="app/admin/page.tsx" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function AdminPage() { const user = await hexclaveServerApp.getUser({ or: "throw" }); const canAccessAdmin = await user.hasPermission("access_admin_dashboard"); if (!canAccessAdmin) { return
Access denied
; } return
Admin dashboard
; } ``` ### Listing All Project Permissions To get a list of all global permissions a user has, use the `listPermissions` method or the `usePermissions` hook. Pass `{ recursive: false }` if you only want direct grants: ```tsx title="List global permissions on the client" theme={null} "use client"; import { useUser } from "@hexclave/next"; export function DisplayGlobalPermissions() { const user = useUser({ or: 'redirect' }); const permissions = user.usePermissions(); return (
{permissions.map(permission => (
{permission.id}
))}
); } ```
```tsx title="List global permissions on the server" theme={null} import { hexclaveServerApp } from "@/hexclave/server"; export default async function DisplayGlobalPermissions() { const user = await hexclaveServerApp.getUser({ or: 'throw' }); const permissions = await user.listPermissions(); return (
{permissions.map(permission => (
{permission.id}
))}
); } ```
If you only want direct project permission grants, pass `{ recursive: false }`: ```tsx theme={null} const directPermissions = await user.listPermissions({ recursive: false }); ``` ### Granting a Project Permission To grant a global permission to a user, use the `grantPermission` method: ```tsx theme={null} const user = await hexclaveServerApp.getUser(); if (!user) throw new Error("User not found"); await user.grantPermission('access_admin_dashboard'); ``` ### Revoking a Project Permission To revoke a global permission from a user, use the `revokePermission` method: ```tsx theme={null} const user = await hexclaveServerApp.getUser(); if (!user) throw new Error("User not found"); await user.revokePermission('access_admin_dashboard'); ``` ## Direct vs. inherited permissions A permission can be present in two ways: * **Direct** - The user was explicitly granted that permission. * **Inherited** - The user was granted a permission that contains it, directly or recursively. The dashboard definition tables show direct containment only. The SDK can return recursive or direct-only lists: ```tsx theme={null} // Includes inherited permissions const allPermissions = await user.listPermissions(team); // Direct assignments only const directPermissions = await user.listPermissions(team, { recursive: false }); ``` For checks like `hasPermission` and `getPermission`, Hexclave resolves contained permissions recursively so roles work as expected. # Teams Source: https://docs.hexclave.com/guides/apps/teams/overview Manage teams and team members Teams provide a structured way to group users and manage their permissions. Users can belong to multiple teams simultaneously, allowing them to represent departments, B2B customers, or projects. The server can perform all operations on a team, but the client can only carry out some actions if the user has the necessary permissions. This applies to all actions that can be performed on a server/client-side `User` object and a `Team` object. ## Concepts ### Team permissions If you attempt to perform an action without the necessary team permissions, the function will throw an error. Always check if the user has the required permission before performing any action. Learn more about permissions [here](../rbac/overview). Here is an example of how to check if a user has a specific permission on the client ```tsx theme={null} const user = useUser({ or: 'redirect' }); const team = user.useTeam('some-team-id'); if (!team) { return
Team not found
; } const hasPermission = user.usePermission(team, '$invite_members'); if (!hasPermission) { return
No permission
; } // Perform corresponding action like inviting a user ``` ### Team profile A user can have a different profile for each team they belong to (Note this is different to the user's personal profile). This profile contains information like `displayName` and `profileImageUrl`. The team profile can be left empty and it will automatically take the user's personal profile information. The team profile is visible to all the other users in the team that have the `$read_members` permission. ## Retrieving a user's teams You can list all teams a user belongs to using the `listTeams` or `useTeams` functions or fetch a specific team with `getTeam` or `useTeam`. These functions work on both clients and servers. ```tsx theme={null} const user = useUser({ or: 'redirect' }); const allTeams = user.useTeams(); const someTeam = user.useTeam('some-team-id'); // May be null if the user is not a member of this team return (
{allTeams.map(team => (
{team.displayName}
))}
{someTeam ? someTeam.displayName : 'Not a member of this team'}
); ```
```tsx theme={null} const user = await hexclaveServerApp.getUser({ or: 'throw' }); const allTeams = await user.listTeams(); const someTeam = await user.getTeam('some-team-id'); // May be null if the user is not a member of this team return (
{allTeams.map(team => (
{team.displayName}
))}
{someTeam ? someTeam.displayName : 'Not a member of this team'}
```
## Creating a team To create a team, use the `createTeam` function on the `User` object. The user will be added to the team with the default team creator permissions (You can change this on the permissions tab in the Stack dashboard). On the client side, this requires enabling the "client side team creation" on the team settings tab in the Stack dashboard. ```jsx theme={null} const team = await user.createTeam({ displayName: 'New Team', }); ``` To create a team on the server without adding a specific user, use the `createTeam` function on the `ServerApp` object: ```jsx theme={null} const team = await hexclaveServerApp.createTeam({ displayName: 'New Team', }); ``` ## Updating a team You can update a team with the `update` function on the `Team` object. On the client, the user must have the `$update_team` permission to perform this action. ```tsx theme={null} await team.update({ displayName: 'New Name', }); ``` ## Custom team metadata You can store custom metadata on a team object, similar to the user object. The metadata can be any JSON object. * `clientMetadata`: Can be read and updated on both the client and server sides. * `serverMetadata`: Can only be read and updated on the server side. * `clientReadOnlyMetadata`: Can be read on both the client and server sides, but can only be updated on the server side. ```tsx theme={null} await team.update({ clientMetadata: { customField: 'value', }, }); console.log(team.clientMetadata.customField); // 'value' ``` ## List users in a team You can list all users in a team with the `listUsers` function or the `useUsers` hook on the `Team` object. Note that if you want to get the team profile, you need to get it with `user.teamProfile`. On the client, the current user must have the `$read_members` permission in the team to perform this action. ```tsx theme={null} // ... retrieve the team and ensure user has the necessary permissions const users = team.useUsers(); return (
{users.map(user => (
{user.teamProfile.displayName}
))}
); ```
```tsx theme={null} // ... retrieve the team const users = await team.listUsers(); return (
{users.map(user => (
{user.teamProfile.displayName}
))}
); ```
## Get current user's team profile You can get the current user's team profile with the `getTeamProfile` or `useTeamProfile` function on the `User` object. This function returns the team profile for the team with the given ID. ```tsx theme={null} const teamProfile = user.useTeamProfile(team); ``` ```tsx theme={null} const teamProfile = await user.getTeamProfile(team); ``` ## Invite a user to a team You can invite a user to a team using the `inviteUser` function on the `Team` object. The user will receive an email with a link to join the team. On the client side, the current user must have the `$invite_members` permission to perform this action. ```tsx theme={null} await team.inviteUser(email); ``` ## Adding a user to a team If you want to add a user to a team without sending an email, use the `addUser` function on the `ServerTeam` object. This function can only be called on the server side. ```tsx theme={null} await team.addUser(user.id); ``` ## Removing a user from a team You can remove a user from a team with the `removeUser` function on the `Team` object. On the client side, the current user must have the `$remove_members` permission to perform this action. ```tsx theme={null} await team.removeUser(user.id); ``` ## Leaving a team All users can leave a team without any permissions required. ```tsx theme={null} const team = await user.getTeam('some-team-id'); await user.leaveTeam(team); ``` ## Deleting a team You can delete a team with the `delete` function on the `Team` object. On the client side, the current user must have the `$delete_team` permission to perform this action. ```tsx theme={null} await team.delete(); ``` # Team Selection Source: https://docs.hexclave.com/guides/apps/teams/team-selection A user can be a member of multiple teams, so most websites using teams will need a way to select a "current team" that the user is working on. There are two primary methods to accomplish this: * **Deep Link**: Each team has a unique URL, for example, `your-website.com/team/`. When a team is selected, it redirects to a page with that team's URL. * **Current Team**: When a user selects a team, the app stores the team as a global "current team" state. In this way, the URL of the current team might be something like `your-website.com/current-team`, and the URL won't change after switching teams. ## Deep Link Method The deep link method is generally recommended because it avoids some common issues associated with the current team method. If two users share a link while using deep link URLs, the receiving user will always be directed to the correct team's information based on the link. ## Current Team Method While the current team method can be simpler to implement, it has a downside. If a user shares a link, the recipient might see information about the wrong team (if their "current team" is set differently). This method can also cause problems when a user has multiple browser tabs open with different teams. ## Selected Team Switcher To facilitate team selection, Stack provides a component that looks like this: TeamSwitcher You can import and use the `SelectedTeamSwitcher` component for the "current team" method. It updates the `selectedTeam` when a user selects a team: ```jsx theme={null} import { SelectedTeamSwitcher } from "@hexclave/next"; // replace `next` with the correct framework SDK package export function MyPage() { return (
); } ``` To combine the switcher with the deep link method, you can pass in `urlMap` and `selectedTeam`. The `urlMap` is a function to generate a URL based on the team information, and `selectedTeam` is the team that the user is currently working on. This lets you implement "deep link" + "most recent team". The component will update the `user.selectedTeam` with the `selectedTeam` prop: ```jsx theme={null} `/team/${team.id}`} selectedTeam={team} /> ``` To implement the "deep link" + "default team" method, where you update the `selectedTeam` only when the user clicks "set to default team" or similar, pass `noUpdateSelectedTeam`: ```jsx theme={null} `/team/${team.id}`} selectedTeam={team} noUpdateSelectedTeam /> ``` ## Example: Deep Link + Most Recent Team First, create a page at `/app/team/[teamId]/page.tsx` to display information about a specific team: ```jsx theme={null} "use client"; import { useUser, SelectedTeamSwitcher } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function TeamPage({ params }: { params: { teamId: string } }) { const user = useUser({ or: 'redirect' }); const team = user.useTeam(params.teamId); if (!team) { return
Team not found
; } return (
`/team/${team.id}`} selectedTeam={team} />

Team Name: {team.displayName}

You are a member of this team.

); } ``` Next, create a page to display all teams at `/app/team/page.tsx`: ```jsx theme={null} "use client"; import { useRouter } from "next/navigation"; import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package export default function TeamsPage() { const user = useUser({ or: 'redirect' }); const teams = user.useTeams(); const router = useRouter(); const selectedTeam = user.selectedTeam; return (
{selectedTeam && }

All Teams

{teams.map(team => ( ))}
); } ``` Now, if you navigate to `http://localhost:3000/team`, you should be able to see and interact with the teams. # Webhooks Source: https://docs.hexclave.com/guides/apps/webhooks/overview Receive real-time updates when events occur in your Stack project Webhooks are a powerful way to keep your backend in sync with Stack. They allow you to receive real-time updates when events occur in your Stack project, such as when a user or team is created, updated, or deleted. For payload schemas and each webhook event, see the [webhook API reference](/api/webhooks/users/usercreated). ## Setting up webhooks In the Stack dashboard, you can create a webhook endpoint in the "Webhooks" section. After creating this endpoint with your server URL, you will start receiving POST requests with a JSON payload at that endpoint. The event payload will look something like this: ```json theme={null} { "type": "team.created", "data": { "id": "2209422a-eef7-4668-967d-be79409972c5", "display_name": "My Team", ... } } ``` ## Testing webhooks locally You can use services like [Svix Playground](https://www.svix.com/play/) or [Webhook.site](https://webhook.site/) to test the receiving of webhooks or relay them to your local development environment. ## Verifying webhooks To ensure the webhook is coming from Stack (and not from a malicious actor) and is not prone to replay attacks, you should verify the request. Stack signs the webhook payload with a secret key that you can find in the endpoint details on the dashboard. You can verify the signature using the Svix client library. Check out the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how) for instructions on how to verify the signature in JavaScript, Python, Ruby, and other languages. Here are example handlers across the supported frameworks: ```tsx Next.js theme={null} // app/api/webhooks/stack/route.ts import { Webhook } from "svix"; export async function POST(request: Request) { const secret = process.env.STACK_WEBHOOK_SECRET!; const payload = await request.text(); const headers = { "svix-id": request.headers.get("svix-id") ?? "", "svix-timestamp": request.headers.get("svix-timestamp") ?? "", "svix-signature": request.headers.get("svix-signature") ?? "", }; const wh = new Webhook(secret); // Throws on error, returns the verified content on success. const verifiedPayload = wh.verify(payload, headers); return Response.json({ ok: true, type: verifiedPayload.type }); } ``` ```tsx React theme={null} // server/webhooks.ts // Webhooks must be received on a server, even if your frontend is a React app. import express from "express"; import { Webhook } from "svix"; const app = express(); app.use("/api/webhooks/stack", express.text({ type: "application/json" })); app.post("/api/webhooks/stack", (req, res) => { const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET!); const verifiedPayload = wh.verify(req.body, { "svix-id": req.header("svix-id") ?? "", "svix-timestamp": req.header("svix-timestamp") ?? "", "svix-signature": req.header("svix-signature") ?? "", }); res.json({ ok: true, type: verifiedPayload.type }); }); ``` ```javascript Express theme={null} import express from "express"; import { Webhook } from "svix"; const app = express(); app.use("/api/webhooks/stack", express.text({ type: "application/json" })); app.post("/api/webhooks/stack", (req, res) => { const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET); const verifiedPayload = wh.verify(req.body, { "svix-id": req.header("svix-id") ?? "", "svix-timestamp": req.header("svix-timestamp") ?? "", "svix-signature": req.header("svix-signature") ?? "", }); res.json({ ok: true, type: verifiedPayload.type }); }); ``` ```javascript Node.js theme={null} import { createServer } from "node:http"; import { Webhook } from "svix"; createServer(async (req, res) => { if (req.method !== "POST" || req.url !== "/api/webhooks/stack") { res.writeHead(404).end(); return; } const payload = await new Promise((resolve, reject) => { let body = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { body += chunk; }); req.on("end", () => resolve(body)); req.on("error", reject); }); const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET); const verifiedPayload = wh.verify(payload, { "svix-id": req.headers["svix-id"] ?? "", "svix-timestamp": req.headers["svix-timestamp"] ?? "", "svix-signature": req.headers["svix-signature"] ?? "", }); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, type: verifiedPayload.type })); }).listen(3000); ``` ```javascript Vanilla JavaScript theme={null} // server.js // Browser-only JavaScript apps still need a server endpoint to receive webhooks. import { createServer } from "node:http"; import { Webhook } from "svix"; createServer(async (req, res) => { if (req.method !== "POST" || req.url !== "/api/webhooks/stack") { res.writeHead(404).end(); return; } const payload = await new Promise((resolve, reject) => { let body = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { body += chunk; }); req.on("end", () => resolve(body)); req.on("error", reject); }); const wh = new Webhook(process.env.STACK_WEBHOOK_SECRET); const verifiedPayload = wh.verify(payload, { "svix-id": req.headers["svix-id"] ?? "", "svix-timestamp": req.headers["svix-timestamp"] ?? "", "svix-signature": req.headers["svix-signature"] ?? "", }); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, type: verifiedPayload.type })); }).listen(3000); ``` ```python Django theme={null} from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from svix.webhooks import Webhook @csrf_exempt def stack_webhook(request): wh = Webhook(settings.STACK_WEBHOOK_SECRET) verified_payload = wh.verify( request.body.decode("utf-8"), { "svix-id": request.headers["svix-id"], "svix-timestamp": request.headers["svix-timestamp"], "svix-signature": request.headers["svix-signature"], }, ) return JsonResponse({"ok": True, "type": verified_payload["type"]}) ``` ```python FastAPI theme={null} from fastapi import FastAPI, Request from svix.webhooks import Webhook app = FastAPI() @app.post("/api/webhooks/stack") async def stack_webhook(request: Request): payload = await request.body() wh = Webhook(STACK_WEBHOOK_SECRET) verified_payload = wh.verify( payload.decode("utf-8"), { "svix-id": request.headers["svix-id"], "svix-timestamp": request.headers["svix-timestamp"], "svix-signature": request.headers["svix-signature"], }, ) return {"ok": True, "type": verified_payload["type"]} ``` ```python Flask theme={null} from flask import Flask, jsonify, request from svix.webhooks import Webhook app = Flask(__name__) @app.post("/api/webhooks/stack") def stack_webhook(): wh = Webhook(STACK_WEBHOOK_SECRET) verified_payload = wh.verify( request.get_data(as_text=True), { "svix-id": request.headers["svix-id"], "svix-timestamp": request.headers["svix-timestamp"], "svix-signature": request.headers["svix-signature"], }, ) return jsonify({"ok": True, "type": verified_payload["type"]}) ``` If you do not want to install the Svix client library or are using a language that is not supported, you can [verify the signature manually](https://docs.svix.com/receiving/verifying-payloads/how-manual). ## Event types These are the `type` values you may receive. Each links to its API reference page. * [`user.created`](/api/webhooks/users/usercreated) * [`user.updated`](/api/webhooks/users/userupdated) * [`user.deleted`](/api/webhooks/users/userdeleted) * [`team.created`](/api/webhooks/teams/teamcreated) * [`team.updated`](/api/webhooks/teams/teamupdated) * [`team.deleted`](/api/webhooks/teams/teamdeleted) * [`team_membership.created`](/api/webhooks/teams/team_membershipcreated) * [`team_membership.deleted`](/api/webhooks/teams/team_membershipdeleted) * [`team_permission.created`](/api/webhooks/teams/team_permissioncreated) * [`team_permission.deleted`](/api/webhooks/teams/team_permissiondeleted) ## Examples Some members of the community have shared their webhook implementations. For example, [here is an example by Clark Gredona](https://gist.github.com/clarkg/56ffad44949826ae3efe0a431b6021c4) that validates the Webhook schema and update a database user. # FAQ Source: https://docs.hexclave.com/guides/faq Frequently asked questions about Stack # FAQ ## Languages & Frameworks For frontends, Stack supports TypeScript and JavaScript, and also has a Swift SDK in public beta. For backends, Stack has a flexible [REST API](/api/overview) that can be used with any language or framework. Yes! You can use our vanilla JavaScript SDK, or, if the framework is React-based, our React SDK. ## Product Ask yourself about ``: * Is `` open-source? * Is `` developer-friendly, well-documented, and lets you get started in minutes? * Besides authentication, does `` also do authorization and user management (see feature list below)? If you answered "no" to any of these questions, then that's how Hexclave is different from ``. Yes! You can [create users programmatically](/api/server/users/create-user) using our [REST API](/api/overview). ## Other Please carefully read our [CONTRIBUTING.md](https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md). # Using Hexclave with AI Source: https://docs.hexclave.com/guides/getting-started/ai-integration Teach your coding agent how to use Hexclave — via web fetch, an installable skill, or the Hexclave MCP server. This page is for giving your own coding agent (Claude Code, Cursor, Codex, opencode, …) ongoing knowledge of Hexclave so it writes Hexclave code correctly. If you would like to use Hexclave to power your own CLI, see the [CLI Authentication](/guides/apps/authentication/cli-authentication) page. There are three ways to give a coding agent working knowledge of Hexclave. They differ only in how the instructions reach the agent — the knowledge is the same. Paste a one-line prompt — your agent fetches the live skill on demand. Install the Hexclave `SKILL.md` file so your agent loads it automatically. Connect your agent to the Hexclave MCP server for live docs and `ask_hexclave` search. ## Option 1: Web fetch (recommended) Most coding agents now support web fetch out of the box, so you can simply tell your agent to use Hexclave's skill from the internet. To do so, paste the prompt below into your coding agent: ```markdown title="One-shot prompt" theme={null} Fetch https://skill.hexclave.com and follow it for this task. ``` The fetched `SKILL.md` indexes the entire Hexclave docs sidebar and tells the agent to pull fresh content for whichever surface you're touching (auth, orgs/teams, payments, emails, analytics, the CLI). It is purely a reference — paste it whenever you want the agent to use Hexclave knowledge. ## Option 2: Skill Install the skill once so your agent loads it automatically every time Hexclave is relevant — no copy-paste required. The fastest way to install the Hexclave skill for a Claude Code-style agent or similar: ```sh title="Terminal" theme={null} npx -y skills add hexclave/hexclave --skill hexclave ``` This detects your agent and installs the skill in the correct location (per-project or global) automatically. From then on, your agent picks it up whenever the conversation touches Hexclave. If your agent can't run the `npx` command above, paste the prompt below into the chat. The agent will fetch `SKILL.md` and drop it into the right location for your agent — per-project if a project-scoped agent folder exists, global otherwise. ````markdown title="Skill install prompt" theme={null} Install the Hexclave skill (`SKILL.md`) for me so you load it automatically on future Hexclave tasks. The canonical file lives at: https://raw.githubusercontent.com/hexclave/hexclave/dev/skills/hexclave/SKILL.md These are the same install locations the `skills` CLI (https://github.com/vercel-labs/skills) uses — match them exactly so the file is picked up the same way. Do this in one shot using smart defaults; only stop to ask a single multiple-choice question at the end if I want to change anything. 1. **Detect the agent.** Identify which coding agent is running you (Claude Code, Codex, Cursor, opencode, Windsurf, Roo, Kilo, Amp, Antigravity, Cline, Continue, Crush, Augment, etc.). If you genuinely can't tell, default to Claude Code. 2. **Pick the default scope.** Look for an existing agent folder in the current project root: `.claude/`, `.agents/`, `.cursor/`, `.codex/`, `.opencode/`, `.windsurf/`, `.roo/`, `.kilocode/`, `.augment/`, `.continue/`, `.crush/`, etc. - If any such folder exists → default to **project** scope. - Otherwise → default to **global** scope. 3. **Resolve the install directory.** Write `SKILL.md` to `/hexclave/SKILL.md`, where `` comes from this table (mirrors `vercel-labs/skills/src/agents.ts`): | Agent | Project `` | Global `` | | --- | --- | --- | | Claude Code | `.claude/skills` | `~/.claude/skills` (or `$CLAUDE_CONFIG_DIR/skills`) | | Codex | `.agents/skills` | `~/.codex/skills` (or `$CODEX_HOME/skills`) | | Cursor | `.agents/skills` | `~/.cursor/skills` | | opencode | `.agents/skills` | `~/.config/opencode/skills` (XDG) | | Windsurf | `.windsurf/skills` | `~/.codeium/windsurf/skills` | | Amp | `.agents/skills` | `~/.config/agents/skills` (XDG) | | Antigravity | `.agents/skills` | `~/.gemini/antigravity/skills` | | Cline | `.agents/skills` | `~/.agents/skills` | | Roo Code | `.roo/skills` | `~/.roo/skills` | | Kilo Code | `.kilocode/skills` | `~/.kilocode/skills` | | Augment | `.augment/skills` | `~/.augment/skills` | | Continue | `.continue/skills` | `~/.continue/skills` | | Crush | `.crush/skills` | `~/.config/crush/skills` | For any agent not listed, follow its documented skills directory or fall back to `.agents/skills` (project) / `~/./skills` (global). 4. **Install.** Download `SKILL.md` once to a stable cache location — `~/.cache/hexclave/SKILL.md` (or `$XDG_CACHE_HOME/hexclave/SKILL.md`) — then create a **symlink** at `/hexclave/SKILL.md` pointing to that cached file. Create missing parent directories. If the target path already exists, replace it (overwrite the symlink, or back up and replace a regular file). Symlinking means future re-installs at additional scopes/agents reuse the same source file, and re-running the prompt after an update to the cached file picks up automatically. If the platform doesn't support symlinks (e.g. Windows without dev mode), fall back to copying the file verbatim. 5. **Confirm and offer alternatives.** Print the absolute symlink path and the cache target it points to. Then ask me a single multiple-choice question covering anything I might want to change, e.g.: ``` Installed Hexclave skill at . Want to change anything? a) Keep it — done b) Also symlink at the opposite scope () c) Install for a different agent instead d) Symlink into additional agents too ``` Only act on whichever letter I pick. Don't pre-ask before the first install. Additional installs just create more symlinks pointing at the same cached `SKILL.md`. Installing the file is the only goal of this prompt — do not act on the skill's contents yet. ```` ## Option 3: MCP The Hexclave MCP server (`https://mcp.hexclave.com/mcp`) gives your agent live access to Hexclave docs and skill via prompts, resources and an `ask_hexclave` tool that searches the docs with citations. Install it once per agent — it stays available across every project. Opens Cursor and adds `stack-auth` to your MCP config. Opens VS Code and adds `stack-auth` to your MCP config. For the up-to-date list of one-click install buttons for every client (Cursor, VS Code, Claude Desktop, Windsurf, ChatGPT, Gemini, …), visit [`mcp.hexclave.com`](https://mcp.hexclave.com) in your browser. Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): ```json title="mcp.json" theme={null} { "mcpServers": { "stack-auth": { "url": "https://mcp.hexclave.com/mcp" } } } ``` ```sh title="Terminal" theme={null} claude mcp add --transport http stack-auth https://mcp.hexclave.com/mcp ``` ```sh title="Terminal" theme={null} code --add-mcp '{"type":"http","name":"stack-auth","url":"https://mcp.hexclave.com/mcp"}' ``` ```sh title="Terminal" theme={null} codex mcp add stack-auth --url https://mcp.hexclave.com/mcp ``` Or add to `~/.codex/config.toml`: ```toml title="config.toml" theme={null} [mcp_servers.stack-auth] url = "https://mcp.hexclave.com/mcp" ``` ```json title="mcp.json" theme={null} { "mcpServers": { "stack-auth": { "serverUrl": "https://mcp.hexclave.com/mcp" } } } ``` Add to `~/.gemini/settings.json`: ```json title="settings.json" theme={null} { "mcpServers": { "stack-auth": { "httpUrl": "https://mcp.hexclave.com/mcp", "headers": { "Accept": "application/json, text/event-stream" } } } } ``` For Claude Desktop, ChatGPT, and copy-paste markdown blocks you can drop into your project README, visit [`mcp.hexclave.com`](https://mcp.hexclave.com). If your agent can edit its own config, paste this prompt: ```markdown title="MCP install prompt" theme={null} Install the Hexclave MCP server for me. The server URL is https://mcp.hexclave.com/mcp (HTTP transport). Detect which coding agent I'm using and add the server to the correct config file: - Claude Code → run `claude mcp add --transport http stack-auth https://mcp.hexclave.com/mcp` - Cursor → `~/.cursor/mcp.json` - VS Code → run `code --add-mcp '{"type":"http","name":"stack-auth","url":"https://mcp.hexclave.com/mcp"}'` - Codex → `~/.codex/config.toml` - opencode → `~/.config/opencode/opencode.json` - Gemini CLI → `~/.gemini/settings.json` If this repo already has a project-scoped MCP config (`.cursor/mcp.json`, `.vscode/mcp.json`, `.claude.json`, `.codex/config.toml`, etc.), install it there instead of the global location. After installing, confirm by listing the registered MCP servers. ``` ## Project vs. global scope The install prompts above follow the same rule for picking project-vs-global scope: | Detected in project root | Skill / MCP install scope | | ------------------------------------------------------------------- | ----------------------------------------------------- | | `.claude/`, `.cursor/`, `.vscode/mcp.json`, `.codex/`, `.opencode/` | **Project** (inside the existing folder) | | None of the above | **Global** (user-level config for the detected agent) | This keeps shared repos clean (project-scoped config lives next to the code) and keeps your personal projects ergonomic (one global install, available everywhere). # Setup Source: https://docs.hexclave.com/guides/getting-started/setup Install and configure Hexclave for your project

Setting up with AI? Use this single prompt:

Choose your tech stack

Choose all that apply.

Filter:

Frontend

Backend

Database

Other

Select a tool to show setup instructions.

Setting up with AI? Use this single prompt in your coding agent to set up Hexclave for your selected stack.
## Next.js SDK Setup Instructions Follow these instructions in order to set up and get started with the Hexclave SDK for Next.js . Note: These instructions are for setting up the Hexclave SDK to build your own CLIs. If you're looking to use the Hexclave CLI instead, see the [CLI documentation](https://docs.hexclave.com/guides/going-further/cli). First, install the `@hexclave/next` npm package with your preferred package manager: ```sh theme={null} npm i @hexclave/next # or: pnpm i @hexclave/next # or: yarn add @hexclave/next # or: bun add @hexclave/next ``` Next, let us create the Hexclave App object for your project. This is the most important object in a Hexclave project. In a frontend where you cannot keep a secret key safe, you would use the `HexclaveClientApp` constructor: ```ts src/hexclave/client.ts theme={null} import { HexclaveClientApp } from "@hexclave/next"; export const hexclaveClientApp = new HexclaveClientApp({ tokenStore: "cookie", // "nextjs-cookie" for Next.js, "cookie" for other web frontends, null for backend environments urls: { default: { type: "hosted", } }, }); ``` In a backend where you can keep a secret key safe, you can use the `HexclaveServerApp`, which provides access to more sensitive APIs compared to `HexclaveClientApp`: ```ts src/hexclave/server.ts theme={null} import { HexclaveServerApp } from "@hexclave/next"; import { hexclaveClientApp } from "./client"; export const hexclaveServerApp = new HexclaveServerApp({ inheritsFrom: hexclaveClientApp, }); ``` It's now time to connect your code to a Hexclave project. You can either run Hexclave's dev environment locally, or connect to a production project hosted in the cloud. First, create a `hexclave.config.ts` configuration file in the root directory of the workspace (or anywhere else): ```ts hexclave.config.ts theme={null} import type { HexclaveConfig } from "@hexclave/next"; // default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts export const config: HexclaveConfig = "show-onboarding"; ``` If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig`, imported from the same `@hexclave/next` package. To run your application with Hexclave, you can then start the dev environment and set environment variables expected by your application. Hexclave's CLI has a `dev` command does both of these, so let's install it as a dev dependency and wrap your existing `dev` script in your package.json: ```sh theme={null} npm i -D @hexclave/cli # or: pnpm i -D @hexclave/cli # or: yarn add -D @hexclave/cli # or: bun add --dev @hexclave/cli ``` ```json package.json theme={null} { // ... "scripts": { // ... "dev": "hexclave dev --config-file ./hexclave.config.ts -- npm run dev:inner", "dev:inner": "" } } ``` `hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup. If you're looking to run a production version of your application, or the local dashboard doesn't work for you, you can also connect to Hexclave's cloud directly. This process is slightly different depending on whether you're setting up a frontend or a backend (whether your app can keep a secret key safe or not). #### Frontend Go to your project's dashboard on [app.hexclave.com](https://app.hexclave.com) and get the project ID. You can find it in the URL after the `/projects/` part. Copy-paste it into your `.env.local` file (or wherever your environment variables are stored): ```.env .env.local theme={null} NEXT_PUBLIC_HEXCLAVE_PROJECT_ID= ``` Alternatively, you can also just set the project ID in the `hexclave/client.ts` file: ```ts src/hexclave/client.ts theme={null} export const hexclaveClientApp = new HexclaveClientApp({ // ... projectId: "your-project-id", }); ``` #### Backend (or both frontend and backend) First, navigate to the [Project Keys](https://app.hexclave.com/projects/-selector-/project-keys) page in the Hexclave dashboard and generate a new set of keys. Then, copy-paste them into your `.env.local` file (or wherever your environment variables are stored): ```.env .env.local theme={null} NEXT_PUBLIC_HEXCLAVE_PROJECT_ID= HEXCLAVE_SECRET_SERVER_KEY= ``` They'll automatically be picked up by the `HexclaveServerApp` constructor. In React frameworks, Hexclave provides `HexclaveProvider` and `HexclaveTheme` components that should wrap your entire app at the root level. You can do this in the `layout.tsx` file in the `app` directory. The root layout must render the `` and `` tags, and `HexclaveProvider`/`HexclaveTheme` must go inside: ```tsx src/app/layout.tsx theme={null} import { HexclaveProvider, HexclaveTheme } from "@hexclave/next"; import { hexclaveServerApp } from "@/hexclave/server"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Hexclave also provides additional `useXyz` React hooks for `getXyz`/`listXyz` functions. For example, `useUser` is like `getUser`, but as a suspending React hook. To support the suspension, you need to add a suspense boundary around your app. In Next.js, this can be easily done by adding a `loading.tsx` file in the `app` directory: ```tsx src/app/loading.tsx theme={null} export default function Loading() { return
Loading...
; } ```
## React SDK Setup Instructions Follow these instructions in order to set up and get started with the Hexclave SDK for React . Note: These instructions are for setting up the Hexclave SDK to build your own CLIs. If you're looking to use the Hexclave CLI instead, see the [CLI documentation](https://docs.hexclave.com/guides/going-further/cli). First, install the `@hexclave/react` npm package with your preferred package manager: ```sh theme={null} npm i @hexclave/react # or: pnpm i @hexclave/react # or: yarn add @hexclave/react # or: bun add @hexclave/react ``` Next, let us create the Hexclave App object for your project. This is the most important object in a Hexclave project. In a frontend where you cannot keep a secret key safe, you would use the `HexclaveClientApp` constructor: ```ts src/hexclave/client.ts theme={null} import { HexclaveClientApp } from "@hexclave/react"; export const hexclaveClientApp = new HexclaveClientApp({ tokenStore: "cookie", // "nextjs-cookie" for Next.js, "cookie" for other web frontends, null for backend environments urls: { default: { type: "hosted", } }, }); ``` It's now time to connect your code to a Hexclave project. You can either run Hexclave's dev environment locally, or connect to a production project hosted in the cloud. First, create a `hexclave.config.ts` configuration file in the root directory of the workspace (or anywhere else): ```ts hexclave.config.ts theme={null} import type { HexclaveConfig } from "@hexclave/react"; // default: show-onboarding, which shows the onboarding flow for this project when Hexclave starts export const config: HexclaveConfig = "show-onboarding"; ``` If you later switch to a config object and want type-checking, wrap it with `defineHexclaveConfig`, imported from the same `@hexclave/react` package. To run your application with Hexclave, you can then start the dev environment and set environment variables expected by your application. Hexclave's CLI has a `dev` command does both of these, so let's install it as a dev dependency and wrap your existing `dev` script in your package.json: ```sh theme={null} npm i -D @hexclave/cli # or: pnpm i -D @hexclave/cli # or: yarn add -D @hexclave/cli # or: bun add --dev @hexclave/cli ``` ```json package.json theme={null} { // ... "scripts": { // ... "dev": "hexclave dev --config-file ./hexclave.config.ts -- npm run dev:inner", "dev:inner": "" } } ``` `hexclave dev` injects all necessary environment variables into the app process automatically, so the app is ready to use without any extra environment variable setup. If you're looking to run a production version of your application, or the local dashboard doesn't work for you, you can also connect to Hexclave's cloud directly. This process is slightly different depending on whether you're setting up a frontend or a backend (whether your app can keep a secret key safe or not). #### Frontend Go to your project's dashboard on [app.hexclave.com](https://app.hexclave.com) and get the project ID. You can find it in the URL after the `/projects/` part. Copy-paste it into your `.env.local` file (or wherever your environment variables are stored): ```.env .env.local theme={null} # note: prefix the environment variable with NEXT_PUBLIC_ or VITE_ if your framework requires you to do so HEXCLAVE_PROJECT_ID= ``` Alternatively, you can also just set the project ID in the `hexclave/client.ts` file: ```ts src/hexclave/client.ts theme={null} export const hexclaveClientApp = new HexclaveClientApp({ // ... projectId: "your-project-id", }); ``` #### Backend (or both frontend and backend) First, navigate to the [Project Keys](https://app.hexclave.com/projects/-selector-/project-keys) page in the Hexclave dashboard and generate a new set of keys. Then, copy-paste them into your `.env.local` file (or wherever your environment variables are stored): ```.env .env.local theme={null} # as above, prefix the project ID environment variable with NEXT_PUBLIC_ or VITE_ if your framework requires you to do so # do NOT prefix the secret server key environment variable with NEXT_PUBLIC_ or VITE_ as it is server-only HEXCLAVE_PROJECT_ID= HEXCLAVE_SECRET_SERVER_KEY= ``` They'll automatically be picked up by the `HexclaveServerApp` constructor. In React frameworks, Hexclave provides `HexclaveProvider` and `HexclaveTheme` components that should wrap your entire app at the root level. For example, if you have an `App.tsx` file, update it as follows: ```tsx src/App.tsx theme={null} import { HexclaveProvider, HexclaveTheme } from "@hexclave/react"; import { hexclaveClientApp } from "./hexclave/client"; export default function App() { return ( {/* your app content */} ); } ``` Hexclave also provides additional `useXyz` React hooks for `getXyz`/`listXyz` functions. For example, `useUser` is like `getUser`, but as a suspending React hook. To support the suspension, you need to add a suspense boundary around your app. The easiest way to do this is to just wrap your entire app in a `Suspense` component: ```tsx src/App.tsx theme={null} import { Suspense } from "react"; import { HexclaveProvider, HexclaveTheme } from "@hexclave/react"; import { hexclaveClientApp } from "./hexclave/client"; export default function App() { return ( Loading...
}> {/* your app content */} ); } ```
## Other JS/TS SDK Setup Instructions Follow these instructions in order to set up and get started with the Hexclave SDK for Other JS/TS . Note: These instructions are for setting up the Hexclave SDK to build your own CLIs. If you're looking to use the Hexclave CLI instead, see the [CLI documentation](https://docs.hexclave.com/guides/going-further/cli). First, install the `@hexclave/js` npm package with your preferred package manager: ```sh theme={null} npm i @hexclave/js # or: pnpm i @hexclave/js # or: yarn add @hexclave/js # or: bun add @hexclave/js ``` Next, let us create the Hexclave App object for your project. This is the most important object in a Hexclave project. In a frontend where you cannot keep a secret key safe, you would use the `HexclaveClientApp` constructor: ```ts src/hexclave/client.ts theme={null} import { HexclaveClientApp } from "@hexclave/js"; export const hexclaveClientApp = new HexclaveClientApp({ tokenStore: "cookie", // "nextjs-cookie" for Next.js, "cookie" for other web frontends, null for backend environments urls: { default: { type: "hosted", } }, }); ``` #### Browser ` ``` Any other script on the page can then use the app through the global, for example `await globalThis.hexclaveClientApp.getUser()`. Important caveats for this approach: * Without a bundler or build step, there is no concept of environment variables in a browser. If there is no build step to inject `HEXCLAVE_PROJECT_ID` and related variables, you must hard-code `projectId` (and `publishableClientKey` if `requirePublishableClientKey` is enabled) directly in the constructor, or inject the environment variables either at build or request time in the server that serves the static file. * Only ever construct a `HexclaveClientApp` here, never a `HexclaveServerApp`, since a `