From 922421554b4ff5e81d9f590488680ac5b54772b2 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 06:17:11 -0500 Subject: [PATCH 01/40] Move fraud protection under authentication apps section --- docs-mintlify/docs.json | 6 +++++- .../overview.mdx => authentication/fraud-protection.mdx} | 5 ++--- docs-mintlify/snippets/docs-apps-home-grid.jsx | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) rename docs-mintlify/guides/apps/{fraud-protection/overview.mdx => authentication/fraud-protection.mdx} (63%) diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index f57b3cbd9..87cc82da0 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -93,6 +93,7 @@ "guides/apps/authentication/connected-accounts", "guides/apps/authentication/jwts", "guides/apps/authentication/sign-up-rules", + "guides/apps/authentication/fraud-protection", "guides/apps/authentication/cli-authentication", { "group": "All Auth Providers", @@ -127,7 +128,6 @@ "guides/apps/teams/team-selection" ] }, - "guides/apps/fraud-protection/overview", "guides/apps/rbac/overview", "guides/apps/api-keys/overview", "guides/apps/data-vault/overview", @@ -305,6 +305,10 @@ { "source": "/guides/going-further/stack-app", "destination": "/sdk/objects/hexclave-app" + }, + { + "source": "/guides/apps/fraud-protection/overview", + "destination": "/guides/apps/authentication/fraud-protection" } ] } diff --git a/docs-mintlify/guides/apps/fraud-protection/overview.mdx b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx similarity index 63% rename from docs-mintlify/guides/apps/fraud-protection/overview.mdx rename to docs-mintlify/guides/apps/authentication/fraud-protection.mdx index ae983c28b..26771bee7 100644 --- a/docs-mintlify/guides/apps/fraud-protection/overview.mdx +++ b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx @@ -1,12 +1,11 @@ --- title: Fraud Protection description: Protect your project from fraud and abuse -icon: "/images/app-icons/fraud-protection.svg" --- Fraud Protection helps you block abusive sign-ups and enforce safer onboarding rules. Use the Authentication app docs below for the implementation details currently available: -- [Sign-up Rules](../authentication/sign-up-rules) -- [Authentication Overview](../authentication/overview) +- [Sign-up Rules](./sign-up-rules) +- [Authentication Overview](./overview) diff --git a/docs-mintlify/snippets/docs-apps-home-grid.jsx b/docs-mintlify/snippets/docs-apps-home-grid.jsx index 2339423c9..050b0a42a 100644 --- a/docs-mintlify/snippets/docs-apps-home-grid.jsx +++ b/docs-mintlify/snippets/docs-apps-home-grid.jsx @@ -21,7 +21,6 @@ export const DocsAppsHomeGrid = () => { { name: "Payments", href: "/guides/apps/payments/overview", iconSrc: "/images/app-icons/payments.svg" }, { name: "Analytics", href: "/guides/apps/analytics/overview", iconSrc: "/images/app-icons/analytics.svg" }, { name: "Teams", href: "/guides/apps/teams/overview", iconSrc: "/images/app-icons/teams.svg" }, - { name: "Fraud Protection", href: "/guides/apps/fraud-protection/overview", iconSrc: "/images/app-icons/fraud-protection.svg" }, { name: "RBAC", href: "/guides/apps/rbac/overview", iconSrc: "/images/app-icons/rbac.svg" }, { name: "API Keys", href: "/guides/apps/api-keys/overview", iconSrc: "/images/app-icons/api-keys.svg" }, { name: "Data Vault", href: "/guides/apps/data-vault/overview", iconSrc: "/images/app-icons/data-vault.svg" }, From f1b54addb58d5e6cfc5ce79524ab26c2439db355 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 06:30:44 -0500 Subject: [PATCH 02/40] Update fruad protection docs --- .../apps/authentication/fraud-protection.mdx | 127 +++++++++++++++++- 1 file changed, 121 insertions(+), 6 deletions(-) diff --git a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx index 26771bee7..835a33f8d 100644 --- a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx +++ b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx @@ -1,11 +1,126 @@ --- -title: Fraud Protection -description: Protect your project from fraud and abuse +title: "Fraud Protection" +description: "Detect bots, free-trial abuse, and other fraudulent sign-ups." +sidebarTitle: "Fraud Protection" --- -Fraud Protection helps you block abusive sign-ups and enforce safer onboarding rules. +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, geo-IP country, and Cloudflare Turnstile result), and the conditions you can write against them in [Sign-up Rules](./sign-up-rules). -Use the Authentication app docs below for the implementation details currently available: +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. -- [Sign-up Rules](./sign-up-rules) -- [Authentication Overview](./overview) + + Fraud Protection inherits its enabled state from its parent app, Authentication. If Authentication is 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` | Geo-IP country of the request (e.g. `"US"`, `"DE"`, `"NG"`). Empty if it can't be resolved. | +| `riskScores.bot` | number (0-100) | `equals`, `not_equals`, `greater_than`, `greater_or_equal`, `less_than`, `less_or_equal` | Confidence that the sign-up is automated. Higher = more likely a bot. Hexclave uses signals like the Cloudflare Turnstile verdict to compute this score. | +| `riskScores.free_trial_abuse` | number (0-100) | same as `riskScores.bot` | Confidence that the user is attempting to abuse a free-trial / new-account incentive (multi-accounting, disposable infra, etc.). | + +The rule tester additionally exposes a **Turnstile** override (`ok` / `invalid` / `error`) so you can simulate how a given Turnstile verdict affects the resulting bot score. + +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** - see [Sign-up Rules → Restrict](./sign-up-rules#restrict) for how restricted users appear in JWTs and the dashboard. + +### 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. +- **Turnstile** - `Default (real result)` / `OK` / `Invalid` / `Error`. The tester doesn't run a live Turnstile check; when left at the default, the simulated verdict falls back to `Invalid` for score derivation, so set an explicit value to model a passing challenge. + +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. +- [JWT Tokens](./jwts) - how the `Restrict` action surfaces in user tokens. From dafe89686a75de380b91b5a3e721713397b90471 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 06:43:12 -0500 Subject: [PATCH 03/40] add queries and tables guide --- docs-mintlify/docs.json | 9 +- .../apps/analytics/queries-and-tables.mdx | 291 ++++++++++++++++++ 2 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 docs-mintlify/guides/apps/analytics/queries-and-tables.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 87cc82da0..0c71fbde9 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -119,7 +119,14 @@ }, "guides/apps/emails/overview", "guides/apps/payments/overview", - "guides/apps/analytics/overview", + { + "group": "Analytics", + "icon": "/images/app-icons/analytics.svg", + "pages": [ + "guides/apps/analytics/overview", + "guides/apps/analytics/queries-and-tables" + ] + }, { "group": "Teams", "icon": "/images/app-icons/teams.svg", diff --git a/docs-mintlify/guides/apps/analytics/queries-and-tables.mdx b/docs-mintlify/guides/apps/analytics/queries-and-tables.mdx new file mode 100644 index 000000000..4cb874e83 --- /dev/null +++ b/docs-mintlify/guides/apps/analytics/queries-and-tables.mdx @@ -0,0 +1,291 @@ +--- +title: "Queries & Tables" +description: "Inspect raw analytics rows and run read-only ClickHouse SQL against your project's dataset" +sidebarTitle: "Queries & Tables" +--- + +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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. From 1b62fbcdb4538740f520d3b3532a09cdd895cffc Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 09:37:28 -0500 Subject: [PATCH 04/40] skeletons, and auth overview --- .../guides/apps/authentication/overview.mdx | 86 +++++++- .../snippets/skeletons/auth-skeletons.jsx | 188 ++++++++++++++++++ 2 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 docs-mintlify/snippets/skeletons/auth-skeletons.jsx diff --git a/docs-mintlify/guides/apps/authentication/overview.mdx b/docs-mintlify/guides/apps/authentication/overview.mdx index 0461ac0bf..94bbfbc53 100644 --- a/docs-mintlify/guides/apps/authentication/overview.mdx +++ b/docs-mintlify/guides/apps/authentication/overview.mdx @@ -1,7 +1,85 @@ --- -title: Authentication -description: Integrate Hexclave with AI-powered tools and services -sidebarTitle: Overview +title: "Authentication" +description: "Production-ready sign-in, a real user directory, and session verification - without building auth yourself" +sidebarTitle: "Overview" --- -TODO stub +import { SignInSkeleton, AuthMethodsSkeleton, UserDirectorySkeleton } from "/snippets/skeletons/auth-skeletons.jsx"; + +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. Mount one catch-all route and you get a fully styled, accessible, maintained auth experience: sign-in, sign-up, password reset, OAuth callbacks, email verification, and account settings - all of it. + +```tsx title="app/handler/[...hexclave]/page.tsx" +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) + + + +Use Hexclave's shared development keys for the major providers (Google, GitHub, Microsoft, Spotify) while you build, then swap in your own credentials for production. See [all auth providers](./auth-providers) for setup details. + +## 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 +// 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 +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 whether you use the managed service or your own deployment. + +## Start here + +1. [Set up Hexclave](/guides/getting-started/setup) in your project (a few minutes). +2. Mount `` at `/handler/[...]` and turn on the auth methods you want. +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. diff --git a/docs-mintlify/snippets/skeletons/auth-skeletons.jsx b/docs-mintlify/snippets/skeletons/auth-skeletons.jsx new file mode 100644 index 000000000..72b10fa1d --- /dev/null +++ b/docs-mintlify/snippets/skeletons/auth-skeletons.jsx @@ -0,0 +1,188 @@ +// Lightweight, decorative skeletons used to illustrate the Authentication app. +// They are intentionally non-interactive and use neutral placeholders so they +// read as "examples" rather than live UI. +// +// NOTE: Mintlify evaluates each exported component in isolation, so every +// component must be fully self-contained — no shared module-level constants or +// helper components. That's why ACCENT / Frame are redefined inside each one. + +export const SignInSkeleton = () => { + const ACCENT = "#6b5df7"; + + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const provider = (label) => ( +
+
+ {label} +
+ ); + + const field = (label) => ( +
+ {label} +
+
+ ); + + return ( +
+ +
+
+
+
Sign in
+
+
+ {provider("Google")} + {provider("GitHub")} +
+
+
+ or +
+
+ {field("Email")} + {field("Password")} +
+ Continue +
+
+ +
+ ); +}; + +export const AuthMethodsSkeleton = () => { + const ACCENT = "#6b5df7"; + + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const toggle = (on) => ( +
+
+
+ ); + + const row = (label, on) => ( +
+
+
+ {label} +
+ {toggle(on)} +
+ ); + + return ( +
+ +
+ {row("Email & password", true)} + {row("Magic link / OTP", true)} + {row("Passkey", true)} + {row("Google", true)} + {row("GitHub", true)} + {row("Microsoft", false)} +
+ +
+ ); +}; + +export const UserDirectorySkeleton = () => { + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const cell = (w) =>
; + + const badge = (text, tone) => { + const tones = { + green: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400", + zinc: "bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300", + }; + return ( + + {text} + + ); + }; + + const row = (initial, color, w1, w2, status) => ( +
+
+
+ {initial} +
+ {cell(w1)} +
+ {cell(w2)} +
{badge(status[0], status[1])}
+
+ ); + + return ( +
+ +
+
+ User + Email + Status +
+ {row("A", "#6b5df7", "68px", "120px", ["Active", "green"])} + {row("M", "#0ea5e9", "84px", "104px", ["Active", "green"])} + {row("S", "#f59e0b", "56px", "132px", ["Invited", "zinc"])} + {row("R", "#ef4444", "72px", "92px", ["Active", "green"])} +
+ +
+ ); +}; From 25ebe2fcda8153a2347d2263e257f4b5d4e893ed Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 10:01:19 -0500 Subject: [PATCH 05/40] Email overview and guide --- docs-mintlify/docs.json | 9 +- docs-mintlify/guides/apps/emails/guide.mdx | 356 ++++++++++++++++++ docs-mintlify/guides/apps/emails/overview.mdx | 340 +++-------------- .../snippets/skeletons/email-skeletons.jsx | 153 ++++++++ 4 files changed, 563 insertions(+), 295 deletions(-) create mode 100644 docs-mintlify/guides/apps/emails/guide.mdx create mode 100644 docs-mintlify/snippets/skeletons/email-skeletons.jsx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 0c71fbde9..9a5786cd5 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -117,7 +117,14 @@ } ] }, - "guides/apps/emails/overview", + { + "group": "Emails", + "icon": "/images/app-icons/emails.svg", + "pages": [ + "guides/apps/emails/overview", + "guides/apps/emails/guide" + ] + }, "guides/apps/payments/overview", { "group": "Analytics", diff --git a/docs-mintlify/guides/apps/emails/guide.mdx b/docs-mintlify/guides/apps/emails/guide.mdx new file mode 100644 index 000000000..eb62b45de --- /dev/null +++ b/docs-mintlify/guides/apps/emails/guide.mdx @@ -0,0 +1,356 @@ +--- +title: "Emails" +description: "Send custom emails, manage templates, and track delivery - all from Hexclave." +sidebarTitle: "Guide" +--- + +Hexclave includes a full email system for sending transactional and marketing emails to your users. It handles rendering, delivery, scheduling, notification preferences, and tracking out of the box. + +## Email types + +There are two categories of email: + +- **Transactional** - Required for your app to function (verification, password reset, receipts). Users cannot opt out. +- **Marketing** - Promotional or informational. Always includes an unsubscribe link. Users can opt out. + + + Never send marketing content as transactional emails. Doing so can get your domain blacklisted by spam filters. + + +## Sending emails + +Emails are sent from your server using `hexclaveServerApp.sendEmail()`. You must provide the content (HTML, a template, or a draft) and the recipients. + +### Send to specific users + +```typescript +await hexclaveServerApp.sendEmail({ + userIds: ['user-id-1', 'user-id-2'], + subject: 'Welcome to our platform!', + html: '

Welcome!

Thanks for joining us.

', +}); +``` + +### Send to all users + +```typescript +await hexclaveServerApp.sendEmail({ + allUsers: true, + templateId: 'your-template-id', + subject: 'We just shipped a big update', + variables: { + featureName: 'Dark mode', + }, +}); +``` + +### Send from a dashboard draft + +If you've composed an email in the dashboard's draft editor, you can trigger it programmatically: + +```typescript +await hexclaveServerApp.sendEmail({ + userIds: ['user-id'], + draftId: 'your-draft-id', +}); +``` + +### Full options + +```typescript +await hexclaveServerApp.sendEmail({ + // Recipients - exactly one of these is required: + userIds: ['user-id-1'], // specific users + // allUsers: true, // or all users in your project + + // Content - exactly one of these is required: + html: '

Hello!

', // raw HTML + // templateId: 'template-id', // or a template with variables + // draftId: 'draft-id', // or a dashboard draft + + // Optional: + subject: 'Hello!', + variables: { key: 'value' }, + themeId: 'theme-id', // apply a specific theme + // themeId: null, // use the default theme + // themeId: false, // send with no theme at all + notificationCategoryName: 'Marketing', + scheduledAt: new Date('2025-12-25T00:00:00Z'), +}); +``` + +### `SendEmailOptions` type shape + +```typescript +type SendEmailOptions = + // Common options: + & { + subject?: string; // subject line + themeId?: string | null | false; // theme override + notificationCategoryName?: string; // preference category + variables?: Record; // template variables + scheduledAt?: Date; // delay delivery until this time + } + // Recipients — exactly one: + & ({ userIds: string[] } | { allUsers: true }) + // Content — exactly one: + & ({ html: string } | { templateId: string } | { draftId: string }); +``` + + + `sendEmail` requires a custom email server (SMTP, Resend, or Managed). It cannot be used with the shared development server. + + +### Error handling + +`sendEmail` resolves to `void` and **throws** on failure. The errors it throws carry a stable string `errorCode` you can branch on - catch the cases you care about and rethrow the rest: + +```typescript +try { + await hexclaveServerApp.sendEmail({ + userIds: ['user-id'], + html: '

Hello!

', + subject: 'Test Email', + }); +} catch (error) { + const errorCode = (error as { errorCode?: string }).errorCode; + switch (errorCode) { + case 'REQUIRES_CUSTOM_EMAIL_SERVER': + // Configure a custom email server (SMTP, Resend, or Managed) + break; + 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 + } +} +``` + +## Scheduling + +Pass a `scheduledAt` date to delay delivery. The email enters the pipeline immediately but won't be sent until the scheduled time. + +```typescript +await hexclaveServerApp.sendEmail({ + userIds: ['user-id'], + html: '

Happy New Year!

', + subject: 'Happy New Year!', + scheduledAt: new Date('2026-01-01T00:00:00Z'), +}); +``` + +If `scheduledAt` is omitted, the email is sent as soon as possible. + +## Email pipeline + +Emails are processed asynchronously through a multi-stage pipeline: + +1. **Enqueue** - The email is saved to the outbox with its template, recipients, and scheduling metadata. +2. **Render** - The template TSX is compiled into HTML, subject, and plain text. +3. **Queue** - Rendered emails whose scheduled time has passed are queued for delivery, respecting your project's sending capacity. +4. **Send** - Emails are delivered, honoring notification preferences and skipping users who have unsubscribed. +5. **Track** - Delivery events (sent, bounced, marked as spam) are recorded. + +You can monitor every email's status in the dashboard under **Emails → Sent**. + +## 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 +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 flows. These are used automatically by the built-in authentication components: + +| 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 | + +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 +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. + +## Notification preferences + +Emails are categorized as either **Transactional** or **Marketing**. Users can opt out of Marketing emails but not Transactional ones. + +When sending, specify the category: + +```typescript +await hexclaveServerApp.sendEmail({ + userIds: ['user-id'], + html: '

Check out our new feature!

', + subject: 'Product Updates', + notificationCategoryName: 'Marketing', +}); +``` + +If a user has unsubscribed from Marketing emails, the email will be automatically skipped during delivery. Marketing emails always include an unsubscribe link. + +## React components integration + +Emails integrate with Hexclave UI components automatically (for example verification, password reset, and magic-link flows). +For custom flows, trigger `sendEmail` from your server code: + +```typescript +import { hexclaveServerApp } from '@hexclave/next'; // replace `next` with the correct framework SDK package + +export async function inviteUser(userId: string) { + // sendEmail resolves to void and throws on failure + await hexclaveServerApp.sendEmail({ + userIds: [userId], + templateId: 'invitation-template', + subject: "You're invited!", + variables: { + inviteUrl: 'https://yourapp.com/invite/token123', + }, + }); +} +``` + +## Email server configuration + +Configure your email server in the dashboard under **Emails → Email Settings**. There are four options: + +### Shared (development only) + +The default for new projects. Emails are sent from `noreply@sent-with-hexclave.com` using Hexclave's shared infrastructure. Good for development - not suitable for production. + +### Custom SMTP + +Connect any SMTP provider. Configure: + +- **Host** - e.g. `smtp.sendgrid.net` +- **Port** - typically 587 (STARTTLS) or 465 (implicit TLS) +- **Username** and **Password** +- **Sender email** and **Sender name** + +### Resend + +Connect your [Resend](https://resend.com) account by entering your API key. Hexclave configures the SMTP connection automatically. + +### Managed + +Let Hexclave manage your email domain. Hexclave handles DNS configuration and deliverability for you. Set up requires: + +1. Choose a subdomain (e.g. `mail.yourapp.com`) +2. Add the DNS records Hexclave provides +3. Verify the domain in the dashboard + + + The dashboard tests your email configuration automatically when you save it by sending a test email. + + +## Delivery stats + +Hexclave tracks delivery metrics across multiple time windows (hour, day, week, month): + +- **Sent** - Successfully delivered +- **Bounced** - Rejected by the recipient's mail server +- **Marked as spam** - Recipient flagged the email + +Access these programmatically: + +```typescript +const info = await hexclaveServerApp.getEmailDeliveryStats(); +// info.stats.day.sent, info.stats.day.bounced, etc. +// info.capacity.rate_per_second, info.capacity.is_boost_active, etc. +``` + +Delivery capacity is managed automatically based on your sending reputation. If you need to temporarily increase throughput, you can activate a capacity boost: + +```typescript +await hexclaveServerApp.activateEmailCapacityBoost(); +``` + +## Drafts + +The dashboard includes a full draft editor where you can compose emails visually before sending. Drafts support: + +- TSX source editing with live preview +- Theme selection +- Recipient picker (specific users or all users) +- Scheduling +- Send history per draft + +Once a draft is sent, it's marked as sent and its delivery can be tracked in the outbox. diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 1cc44cdaf..21f8d2352 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -1,173 +1,35 @@ --- title: "Emails" -description: "Send custom emails, manage templates, and track delivery - all from Hexclave." -icon: "/images/app-icons/emails.svg" +description: "Send branded transactional and marketing email - rendering, delivery, and tracking handled for you" +sidebarTitle: "Overview" --- -Hexclave includes a full email system for sending transactional and marketing emails to your users. It handles rendering, delivery, scheduling, notification preferences, and tracking out of the box. +import { EmailPreviewSkeleton, EmailTemplatesSkeleton, DeliveryStatsSkeleton } from "/snippets/skeletons/email-skeletons.jsx"; -## Email types +Email is the other half of auth - verification, password resets, receipts, product updates. The Emails app sends all of it from one server call, with templates, themes, scheduling, unsubscribe handling, and delivery tracking built in. Below are the questions developers actually ask, and the honest answers. -There are two categories of email: +## Can I send an email from my backend? -- **Transactional** - Required for your app to function (verification, password reset, receipts). Users cannot opt out. -- **Marketing** - Promotional or informational. Always includes an unsubscribe link. Users can opt out. - - - Never send marketing content as transactional emails. Doing so can get your domain blacklisted by spam filters. - - -## Sending emails - -Emails are sent from your server using `hexclaveServerApp.sendEmail()`. You must provide the content (HTML, a template, or a draft) and the recipients. - -### Send to specific users +Yes. One call from your server, addressed to specific users or everyone in your project. No SMTP wiring, no render step to manage. ```typescript await hexclaveServerApp.sendEmail({ - userIds: ['user-id-1', 'user-id-2'], - subject: 'Welcome to our platform!', - html: '

Welcome!

Thanks for joining us.

', + userIds: ["user-id"], + subject: "Welcome aboard!", + html: "

Welcome!

Thanks for joining us.

", }); ``` -### Send to all users + -```typescript -await hexclaveServerApp.sendEmail({ - allUsers: true, - templateId: 'your-template-id', - subject: 'We just shipped a big update', - variables: { - featureName: 'Dark mode', - }, -}); -``` +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. -### Send from a dashboard draft +## Can I use my own templates? -If you've composed an email in the dashboard's draft editor, you can trigger it programmatically: - -```typescript -await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - draftId: 'your-draft-id', -}); -``` - -### Full options - -```typescript -await hexclaveServerApp.sendEmail({ - // Recipients - exactly one of these is required: - userIds: ['user-id-1'], // specific users - // allUsers: true, // or all users in your project - - // Content - exactly one of these is required: - html: '

Hello!

', // raw HTML - // templateId: 'template-id', // or a template with variables - // draftId: 'draft-id', // or a dashboard draft - - // Optional: - subject: 'Hello!', - variables: { key: 'value' }, - themeId: 'theme-id', // apply a specific theme - // themeId: null, // use the default theme - // themeId: false, // send with no theme at all - notificationCategoryName: 'Marketing', - scheduledAt: new Date('2025-12-25T00:00:00Z'), -}); -``` - -### `SendEmailOptions` type shape - -```typescript -type SendEmailOptions = { - userIds: string[]; // users to send to - themeId?: string | null | false; // theme override - subject?: string; // subject line - notificationCategoryName?: string; // preference category - html?: string; // raw HTML body - templateId?: string; // template ID - variables?: Record; // template variables -}; -``` - - - `sendEmail` requires a custom email server (SMTP, Resend, or Managed). It cannot be used with the shared development server. - - -### Error handling - -`sendEmail` returns a result object. Handle failures explicitly: - -```typescript -const result = await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - html: '

Hello!

', - subject: 'Test Email', -}); - -if (result.status === 'error') { - switch (result.error.code) { - case 'REQUIRES_CUSTOM_EMAIL_SERVER': - console.error('Please configure a custom email server'); - break; - case 'SCHEMA_ERROR': - console.error('Invalid email data provided'); - break; - case 'USER_ID_DOES_NOT_EXIST': - console.error('One or more user IDs do not exist'); - break; - } -} -``` - -## Scheduling - -Pass a `scheduledAt` date to delay delivery. The email enters the pipeline immediately but won't be sent until the scheduled time. - -```typescript -await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - html: '

Happy New Year!

', - subject: 'Happy New Year!', - scheduledAt: new Date('2026-01-01T00:00:00Z'), -}); -``` - -If `scheduledAt` is omitted, the email is sent as soon as possible. - -## Email pipeline - -Emails are processed asynchronously through a multi-stage pipeline: - -1. **Enqueue** - The email is saved to the outbox with its template, recipients, and scheduling metadata. -2. **Render** - The template TSX is compiled into HTML, subject, and plain text. -3. **Queue** - Rendered emails whose scheduled time has passed are queued for delivery, respecting your project's sending capacity. -4. **Send** - Emails are delivered, honoring notification preferences and skipping users who have unsubscribed. -5. **Track** - Delivery events (sent, opened, clicked, bounced, marked as spam) are recorded. - -You can monitor every email's status in the dashboard under **Emails → Sent**. - -## Templates - -Templates are React Email components written in TSX. Each template receives the current `user`, `project`, and any custom `variables` you pass when sending. +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 -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) { +export function EmailTemplate({ user, variables }: Props) { return ( @@ -176,175 +38,65 @@ export function EmailTemplate({ ); } - -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. +Hexclave also ships ready-made templates for the common auth flows - email verification, password reset, magic link, team invitations, and payment receipts - wired up automatically and customizable from the dashboard. -### Built-in templates +## Can I match my brand? -Hexclave ships with templates for common auth flows. These are used automatically by the built-in authentication components: +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. Set a project default, override per-email with `themeId`, or send with no theme at all. -| 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 | +## Can I respect unsubscribes and preferences? -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 -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. - -## Notification preferences - -Emails are categorized as either **Transactional** or **Marketing**. Users can opt out of Marketing emails but not Transactional ones. - -When sending, specify the category: +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 await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - html: '

Check out our new feature!

', - subject: 'Product Updates', - notificationCategoryName: 'Marketing', + userIds: ["user-id"], + subject: "Product updates", + html: "

Check out what's new!

", + notificationCategoryName: "Marketing", }); ``` -If a user has unsubscribed from Marketing emails, the email will be automatically skipped during delivery. Marketing emails always include an unsubscribe link. +## Can I use my own email provider? -## React components integration +Yes. Connect **custom SMTP**, plug in **Resend** with an API key, or let Hexclave run a **Managed** domain and handle DNS and deliverability for you. 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. -Emails integrate with Hexclave UI components automatically (for example verification, password reset, and magic-link flows). -For custom flows, trigger `sendEmail` from your server code: +## 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 -import { hexclaveServerApp } from '@hexclave/next'; // replace `next` with the correct framework SDK package - -export async function inviteUser(userId: string) { - const result = await hexclaveServerApp.sendEmail({ - userIds: [userId], - templateId: 'invitation-template', - subject: "You're invited!", - variables: { - inviteUrl: 'https://yourapp.com/invite/token123', - }, - }); - - return result; -} +await hexclaveServerApp.sendEmail({ + allUsers: true, + templateId: "product-update", + subject: "We just shipped a big update", + scheduledAt: new Date("2026-01-01T00:00:00Z"), +}); ``` -## Email server configuration +## Can I see what happened after I hit send? -Configure your email server in the dashboard under **Emails → Email Settings**. There are four options: +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. -### Shared (development only) - -The default for new projects. Emails are sent from `noreply@sent-with-hexclave.com` using Hexclave's shared infrastructure. Good for development - not suitable for production. - -### Custom SMTP - -Connect any SMTP provider. Configure: - -- **Host** - e.g. `smtp.sendgrid.net` -- **Port** - typically 587 (STARTTLS) or 465 (implicit TLS) -- **Username** and **Password** -- **Sender email** and **Sender name** - -### Resend - -Connect your [Resend](https://resend.com) account by entering your API key. Hexclave configures the SMTP connection automatically. - -### Managed - -Let Hexclave manage your email domain. Hexclave handles DNS configuration and deliverability for you. Set up requires: - -1. Choose a subdomain (e.g. `mail.yourapp.com`) -2. Add the DNS records Hexclave provides -3. Verify the domain in the dashboard - - - The dashboard tests your email configuration automatically when you save it by sending a test email. - - -## Delivery stats - -Hexclave tracks delivery metrics across multiple time windows (hour, day, week, month): - -- **Sent** - Successfully delivered -- **Bounced** - Rejected by the recipient's mail server -- **Marked as spam** - Recipient flagged the email - -Access these programmatically: + ```typescript const info = await hexclaveServerApp.getEmailDeliveryStats(); -// info.stats.day.sent, info.stats.day.bounced, etc. -// info.capacity.rate_per_second, info.capacity.is_boost_active, etc. +// info.stats.day.sent, info.stats.day.bounced, ... ``` -Delivery capacity is managed automatically based on your sending reputation. If you need to temporarily increase throughput, you can activate a capacity boost: +## Can I compose without writing code? -```typescript -await hexclaveServerApp.activateEmailCapacityBoost(); -``` +Yes. The dashboard has a full draft editor with live preview, theme selection, a recipient picker, and scheduling - then trigger the draft programmatically with `draftId` or send it straight from the dashboard. -## Drafts +## Start here -The dashboard includes a full draft editor where you can compose emails visually before sending. Drafts support: +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. -- TSX source editing with live preview -- Theme selection -- Recipient picker (specific users or all users) -- Scheduling -- Send history per draft - -Once a draft is sent, it's marked as sent and its delivery can be tracked in the outbox. +Ready for the details - every option, the template and theme APIs, the delivery pipeline, and configuration? Read the [Emails guide](./guide). diff --git a/docs-mintlify/snippets/skeletons/email-skeletons.jsx b/docs-mintlify/snippets/skeletons/email-skeletons.jsx new file mode 100644 index 000000000..728dc97dd --- /dev/null +++ b/docs-mintlify/snippets/skeletons/email-skeletons.jsx @@ -0,0 +1,153 @@ +// Lightweight, decorative skeletons used to illustrate the Emails app. +// They are intentionally non-interactive and use neutral placeholders so they +// read as "examples" rather than live UI. +// +// NOTE: Mintlify evaluates each exported component in isolation, so every +// component must be fully self-contained — no shared module-level constants or +// helper components. That's why ACCENT / Frame are redefined inside each one. + +export const EmailPreviewSkeleton = () => { + const ACCENT = "#6b5df7"; + + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const bodyLine = (w) => ( +
+ ); + + return ( +
+ +
+
+
+
+
+
+
+
+ {bodyLine("100%")} + {bodyLine("92%")} + {bodyLine("96%")} + {bodyLine("60%")} +
+
+ Get started +
+
+
+
+
+
+ +
+ ); +}; + +export const EmailTemplatesSkeleton = () => { + const ACCENT = "#6b5df7"; + + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const row = (label, tag) => ( +
+
+
+
+
+ {label} +
+ + {tag} + +
+ ); + + return ( +
+ +
+ {row("Email verification", "Transactional")} + {row("Password reset", "Transactional")} + {row("Magic link / OTP", "Transactional")} + {row("Team invitation", "Transactional")} + {row("Product update", "Marketing")} +
+ +
+ ); +}; + +export const DeliveryStatsSkeleton = () => { + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const tile = (label, value, dot) => ( +
+
+
+ {label} +
+ {value} +
+ {[7, 11, 6, 13, 9, 14, 10].map((h, i) => ( +
+ ))} +
+
+ ); + + return ( +
+ +
+ {tile("Sent", "8,241", "#10b981")} + {tile("Bounced", "37", "#f59e0b")} + {tile("Spam", "4", "#ef4444")} +
+ +
+ ); +}; From bc19e6e53407f1d3d8e122a5dbeabb503e37b352 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 10:07:12 -0500 Subject: [PATCH 06/40] Add agent/llm visible note to overview pages --- docs-mintlify/guides/apps/authentication/overview.mdx | 6 ++++++ docs-mintlify/guides/apps/emails/overview.mdx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs-mintlify/guides/apps/authentication/overview.mdx b/docs-mintlify/guides/apps/authentication/overview.mdx index 94bbfbc53..79ccdba54 100644 --- a/docs-mintlify/guides/apps/authentication/overview.mdx +++ b/docs-mintlify/guides/apps/authentication/overview.mdx @@ -4,6 +4,12 @@ description: "Production-ready sign-in, a real user directory, and session verif sidebarTitle: "Overview" --- + + + **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), [Auth providers](./auth-providers), [JWTs & session verification](./jwts), [Sign-up rules](./sign-up-rules), and [Connected accounts](./connected-accounts). + + + import { SignInSkeleton, AuthMethodsSkeleton, UserDirectorySkeleton } from "/snippets/skeletons/auth-skeletons.jsx"; 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. diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 21f8d2352..d356d6469 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -4,6 +4,12 @@ description: "Send branded transactional and marketing email - rendering, delive sidebarTitle: "Overview" --- + + + **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). + + + import { EmailPreviewSkeleton, EmailTemplatesSkeleton, DeliveryStatsSkeleton } from "/snippets/skeletons/email-skeletons.jsx"; Email is the other half of auth - verification, password resets, receipts, product updates. The Emails app sends all of it from one server call, with templates, themes, scheduling, unsubscribe handling, and delivery tracking built in. Below are the questions developers actually ask, and the honest answers. From 7fa1f2f53fb9215f6e40386d7d2ee4f1134ab599 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 10:12:25 -0500 Subject: [PATCH 07/40] templates-and-themes emails app --- docs-mintlify/docs.json | 3 +- docs-mintlify/guides/apps/emails/guide.mdx | 93 +-------------- .../apps/emails/templates-and-themes.mdx | 109 ++++++++++++++++++ 3 files changed, 114 insertions(+), 91 deletions(-) create mode 100644 docs-mintlify/guides/apps/emails/templates-and-themes.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 9a5786cd5..b3dfd9ae7 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -122,7 +122,8 @@ "icon": "/images/app-icons/emails.svg", "pages": [ "guides/apps/emails/overview", - "guides/apps/emails/guide" + "guides/apps/emails/guide", + "guides/apps/emails/templates-and-themes" ] }, "guides/apps/payments/overview", diff --git a/docs-mintlify/guides/apps/emails/guide.mdx b/docs-mintlify/guides/apps/emails/guide.mdx index eb62b45de..fd7734aad 100644 --- a/docs-mintlify/guides/apps/emails/guide.mdx +++ b/docs-mintlify/guides/apps/emails/guide.mdx @@ -157,98 +157,11 @@ Emails are processed asynchronously through a multi-stage pipeline: You can monitor every email's status in the dashboard under **Emails → Sent**. -## Templates +## Templates & themes -Templates are React Email components written in TSX. Each template receives the current `user`, `project`, and any custom `variables` you pass when sending. +Email content is authored as **templates** (React Email TSX with typed variables) and wrapped in **themes** (shared layout and branding). Hexclave ships built-in templates for the common auth flows and three built-in themes, and you can customize or create your own from the dashboard. Reference a template when sending with `templateId`, and a theme with `themeId`. -```tsx -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 flows. These are used automatically by the built-in authentication components: - -| 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 | - -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 -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. +See [Templates & themes](./templates-and-themes) for the full reference - the template and theme APIs, built-in templates, and the built-in themes. ## Notification preferences diff --git a/docs-mintlify/guides/apps/emails/templates-and-themes.mdx b/docs-mintlify/guides/apps/emails/templates-and-themes.mdx new file mode 100644 index 000000000..c162700c1 --- /dev/null +++ b/docs-mintlify/guides/apps/emails/templates-and-themes.mdx @@ -0,0 +1,109 @@ +--- +title: "Templates & Themes" +description: "Author email content with React Email templates and wrap it in reusable, branded themes." +sidebarTitle: "Templates & 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 +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 flows. These are used automatically by the built-in authentication components: + +| 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 | + +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 +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 + +- [Emails guide](./guide) - sending emails, scheduling, the delivery pipeline, and server configuration. From 15d29a7ddbcc0758d8efcfeb32725d4af67492b5 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 10:14:17 -0500 Subject: [PATCH 08/40] api-keys docs updates --- .../guides/apps/api-keys/overview.mdx | 714 ++++++++++++------ 1 file changed, 466 insertions(+), 248 deletions(-) diff --git a/docs-mintlify/guides/apps/api-keys/overview.mdx b/docs-mintlify/guides/apps/api-keys/overview.mdx index e95161606..fd3ba1541 100644 --- a/docs-mintlify/guides/apps/api-keys/overview.mdx +++ b/docs-mintlify/guides/apps/api-keys/overview.mdx @@ -4,17 +4,26 @@ description: "Create and manage API keys for users and teams" icon: "/images/app-icons/api-keys.svg" --- -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 for users and teams to manage their own API keys. +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. -## Overview +## Concepts -API keys allow your users to access your backend services programmatically without interactive authentication. +### The authentication flow -The flow works as follows: 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 object. Your server then processes the request and returns the response. +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. -Hexclave provides two types of API keys: +### Two kinds of API keys -### User 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. @@ -42,7 +51,7 @@ User API keys are associated with individual users and allow them to authenticat ```typescript title="app/components/create-api-key.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateApiKey() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); @@ -84,16 +93,16 @@ User API keys are associated with individual users and allow them to authenticat def create_user_api_key(request): # Get the current user's access token from session/cookie - access_token = request.COOKIES.get('stack-access-token') + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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', @@ -115,18 +124,18 @@ User API keys are associated with individual users and allow them to authenticat from fastapi import Cookie, HTTPException @app.post("/api/create-user-api-key") - async def create_user_api_key(stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + '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', @@ -149,7 +158,7 @@ User API keys are associated with individual users and allow them to authenticat @app.route('/api/create-user-api-key', methods=['POST']) def create_user_api_key(): - access_token = request.cookies.get('stack-access-token') + access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 @@ -157,10 +166,10 @@ User API keys are associated with individual users and allow them to authenticat response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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', @@ -177,7 +186,7 @@ User API keys are associated with individual users and allow them to authenticat -### Team API keys +#### Team API keys Team API keys are associated with teams and can be used to provide access to team resources over your API. @@ -208,7 +217,7 @@ Team API keys are associated with teams and can be used to provide access to tea ```typescript title="app/components/create-team-api-key.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateTeamApiKey({ teamId }: { teamId: string }) { const team = await hexclaveServerApp.getTeam(teamId); @@ -258,16 +267,16 @@ Team API keys are associated with teams and can be used to provide access to tea def create_team_api_key(request, team_id): # Get the current user's access token from session/cookie - access_token = request.COOKIES.get('stack-access-token') + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, @@ -289,18 +298,18 @@ Team API keys are associated with teams and can be used to provide access to tea from fastapi import Cookie, HTTPException @app.post("/api/teams/{team_id}/api-keys") - async def create_team_api_key(team_id: str, stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + '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, @@ -323,7 +332,7 @@ Team API keys are associated with teams and can be used to provide access to tea @app.route('/api/teams//api-keys', methods=['POST']) def create_team_api_key(team_id): - access_token = request.cookies.get('stack-access-token') + access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 @@ -331,10 +340,10 @@ Team API keys are associated with teams and can be used to provide access to tea response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, @@ -353,52 +362,61 @@ Team API keys are associated with teams and can be used to provide access to tea ## Enabling the API Keys App -To use API keys in your application, you need to enable the API Keys app in your Hexclave dashboard: +To use API keys in your application, you must enable the **API Keys** app in your Hexclave dashboard: -1. Navigate to your Hexclave dashboard -2. Go to the **Apps** section -3. Find and click on **API Keys** in the app store -4. Click the **Enable** button +1. Open your Hexclave dashboard +2. Go to **Apps** +3. Find and open **API Keys** +4. Click **Enable** -Once enabled, you can configure User API Keys and Team API Keys in the app settings. The app will provide your users with a prebuilt UI to manage their own API keys. +### 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 allow your users to manage their own API keys without any additional code: +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 custom descriptions and expiration dates +- 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. +- 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/src/account-page.tsx" + ```typescript title="app/account/page.tsx" import { AccountSettings } from '@hexclave/next'; export default function MyAccountPage() { - return ( - - ); + return ; } ``` - ```typescript title="app/src/account-page.tsx" + ```typescript title="src/account-page.tsx" import { AccountSettings } from '@hexclave/react'; export default function MyAccountPage() { - return ( - - ); + return ; } ``` @@ -406,17 +424,44 @@ For frameworks that support React components, the `` component ### Team API Keys UI -For team API keys, the team settings page automatically includes an API Keys section when: +The team settings page automatically includes an **API Keys** section when **all** of the following are true: - The API Keys app is enabled -- `allowTeamApiKeys` is configured in your project settings -- The user has the `$manage_api_keys` permission for the team +- `apiKeys.enabled.team` is on for your project +- The current user has the `$manage_api_keys` permission on the team -Users with appropriate permissions can manage team API keys directly from the team settings interface. +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 User API Keys +### Creating a user API key @@ -430,7 +475,7 @@ Users with appropriate permissions can manage team API keys directly from the te const handleCreateKey = async () => { const apiKey = await user.createApiKey({ description: "My client application", - expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days + expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days }); console.log("API Key created:", apiKey.value); @@ -442,14 +487,14 @@ Users with appropriate permissions can manage team API keys directly from the te ```typescript title="app/components/create-api-key.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateApiKey() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); const apiKey = await user.createApiKey({ description: "Admin-provisioned API key", - expiresAt: new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)), // 30 days + expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days }); return
API Key: {apiKey.value}
; @@ -467,7 +512,7 @@ Users with appropriate permissions can manage team API keys directly from the te const handleCreateKey = async () => { const apiKey = await user.createApiKey({ description: "My client application", - expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days + expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), }); console.log("API Key created:", apiKey.value); @@ -480,20 +525,21 @@ Users with appropriate permissions can manage team API keys directly from the te ```python title="views.py" 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('stack-access-token') + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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', @@ -515,18 +561,18 @@ Users with appropriate permissions can manage team API keys directly from the te from fastapi import Cookie, HTTPException @app.post("/api/create-user-api-key") - async def create_user_api_key(stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + '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', @@ -549,7 +595,7 @@ Users with appropriate permissions can manage team API keys directly from the te @app.route('/api/create-user-api-key', methods=['POST']) def create_user_api_key(): - access_token = request.cookies.get('stack-access-token') + access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 @@ -557,10 +603,10 @@ Users with appropriate permissions can manage team API keys directly from the te response = requests.post( 'https://api.hexclave.com/api/v1/user-api-keys', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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', @@ -577,7 +623,9 @@ Users with appropriate permissions can manage team API keys directly from the te
-### Creating Team API Keys +### Creating a team API key + +Requires the `$manage_api_keys` team permission. @@ -594,7 +642,7 @@ Users with appropriate permissions can manage team API keys directly from the te const teamApiKey = await team.createApiKey({ description: "Team integration service", - expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days + expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days }); console.log("Team API Key created:", teamApiKey.value); @@ -606,7 +654,7 @@ Users with appropriate permissions can manage team API keys directly from the te ```typescript title="app/components/create-team-api-key.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CreateTeamApiKey({ teamId }: { teamId: string }) { const team = await hexclaveServerApp.getTeam(teamId); @@ -615,9 +663,10 @@ Users with appropriate permissions can manage team API keys directly from the te 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 + expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days }); return
Team API Key: {teamApiKey.value}
; @@ -638,7 +687,7 @@ Users with appropriate permissions can manage team API keys directly from the te const teamApiKey = await team.createApiKey({ description: "Team integration service", - expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days + expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days }); console.log("Team API Key created:", teamApiKey.value); @@ -656,16 +705,16 @@ Users with appropriate permissions can manage team API keys directly from the te def create_team_api_key(request, team_id): # Get the current user's access token from session/cookie - access_token = request.COOKIES.get('stack-access-token') + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, @@ -687,18 +736,18 @@ Users with appropriate permissions can manage team API keys directly from the te from fastapi import Cookie, HTTPException @app.post("/api/teams/{team_id}/api-keys") - async def create_team_api_key(team_id: str, stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + '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, @@ -721,7 +770,7 @@ Users with appropriate permissions can manage team API keys directly from the te @app.route('/api/teams//api-keys', methods=['POST']) def create_team_api_key(team_id): - access_token = request.cookies.get('stack-access-token') + access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 @@ -729,10 +778,10 @@ Users with appropriate permissions can manage team API keys directly from the te response = requests.post( 'https://api.hexclave.com/api/v1/team-api-keys', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, @@ -749,7 +798,7 @@ Users with appropriate permissions can manage team API keys directly from the te
-### Listing API Keys +### Listing API keys @@ -778,7 +827,7 @@ Users with appropriate permissions can manage team API keys directly from the te ```typescript title="app/components/api-keys-list.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function ApiKeysList() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); @@ -830,16 +879,16 @@ Users with appropriate permissions can manage team API keys directly from the te def list_user_api_keys(request): # Get the current user's access token from session/cookie - access_token = request.COOKIES.get('stack-access-token') + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, } ) @@ -855,18 +904,18 @@ Users with appropriate permissions can manage team API keys directly from the te from fastapi import Cookie, HTTPException @app.get("/api/user-api-keys") - async def list_user_api_keys(stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + '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, } ) @@ -883,7 +932,7 @@ Users with appropriate permissions can manage team API keys directly from the te @app.route('/api/user-api-keys', methods=['GET']) def list_user_api_keys(): - access_token = request.cookies.get('stack-access-token') + access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 @@ -891,10 +940,10 @@ Users with appropriate permissions can manage team API keys directly from the te response = requests.get( 'https://api.hexclave.com/api/v1/user-api-keys?user_id=me', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, } ) @@ -906,69 +955,45 @@ Users with appropriate permissions can manage team API keys directly from the te -### Revoking API Keys +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. -API keys can be revoked when they are no longer needed or if they have been compromised. +### 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/components/revoke-api-key.tsx" - "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" - import { hexclaveServerApp } from "@/stack/server"; + ```typescript title="app/api/protected/route.ts" + 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(); + 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="components/RevokeApiKey.tsx" - "use client"; - import { useUser } from "@hexclave/react"; + + ```typescript title="app/api/team-protected/route.ts" + import { hexclaveServerApp } from "@/hexclave/server"; - export default function RevokeApiKey({ apiKeyId }: { apiKeyId: string }) { - const user = useUser({ or: 'redirect' }); - const apiKeys = user.useApiKeys(); + 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 handleRevoke = async () => { - const apiKeyToRevoke = apiKeys.find(key => key.id === apiKeyId); + const team = await hexclaveServerApp.getTeam({ apiKey }); + if (!team) return new Response("Invalid team API key", { status: 401 }); - if (apiKeyToRevoke) { - await apiKeyToRevoke.revoke(); - console.log("API Key revoked"); - } - }; - - return ; + return Response.json({ teamId: team.id, displayName: team.displayName }); } ``` @@ -977,58 +1002,76 @@ API keys can be revoked when they are no longer needed or if they have been comp 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('stack-access-token') + 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 '):] - # 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}', + # Check the API key via server API + check = requests.post( + 'https://api.hexclave.com/api/v1/user-api-keys/check', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + 'x-hexclave-access-type': 'server', + 'x-hexclave-project-id': hexclave_project_id, + 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, - json={ - 'revoked': True, - } + json={'api_key': api_key}, ) - if response.status_code != 200: - raise Exception(f"Failed to revoke API key: {response.text}") + if check.status_code != 200: + return JsonResponse({'error': 'Invalid API key'}, status=401) - return JsonResponse({'message': 'API key revoked successfully'}) + 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" import requests - from fastapi import Cookie, HTTPException + from fastapi import Header, HTTPException - @app.delete("/api/user-api-keys/{api_key_id}") - async def revoke_api_key(api_key_id: str, stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: - raise HTTPException(status_code=401, detail="Not authenticated") + @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 '):] - # 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}', + # Check the API key via server API + check = requests.post( + 'https://api.hexclave.com/api/v1/user-api-keys/check', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + 'x-hexclave-access-type': 'server', + 'x-hexclave-project-id': hexclave_project_id, + 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, - json={ - 'revoked': True, - } + json={'api_key': api_key}, ) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail=response.text) + if check.status_code != 200: + raise HTTPException(status_code=401, detail="Invalid API key") - return {"message": "API key revoked successfully"} + 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')} ``` @@ -1036,37 +1079,45 @@ API keys can be revoked when they are no longer needed or if they have been comp 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('stack-access-token') - if not access_token: - return jsonify({'error': 'Not authenticated'}), 401 + @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 '):] - # 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}', + # Check the API key via server API + check = requests.post( + 'https://api.hexclave.com/api/v1/user-api-keys/check', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + 'x-hexclave-access-type': 'server', + 'x-hexclave-project-id': hexclave_project_id, + 'x-hexclave-secret-server-key': hexclave_secret_server_key, }, - json={ - 'revoked': True, - } + json={'api_key': api_key}, ) - if response.status_code != 200: - return jsonify({'error': response.text}), response.status_code + if check.status_code != 200: + return jsonify({'error': 'Invalid API key'}), 401 - return jsonify({'message': 'API key revoked successfully'}) + 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 API Key Validity +### Checking an existing key's validity -You can check if an API key is still valid: +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`. @@ -1095,7 +1146,7 @@ You can check if an API key is still valid: ```typescript title="app/components/check-api-key.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CheckApiKeyValidity({ userId, @@ -1155,16 +1206,16 @@ You can check if an API key is still valid: def check_api_key_validity(request, api_key_id): # Get the current user's access token from session/cookie - access_token = request.COOKIES.get('stack-access-token') + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, } ) @@ -1198,18 +1249,18 @@ You can check if an API key is still valid: from fastapi import Cookie, HTTPException @app.get("/api/check-api-key/{api_key_id}") - async def check_api_key_validity(api_key_id: str, stack_access_token: str = Cookie(None, alias="stack-access-token")): - if not stack_access_token: + 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-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': stack_access_token, + '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, } ) @@ -1244,7 +1295,7 @@ You can check if an API key is still valid: @app.route('/api/check-api-key/', methods=['GET']) def check_api_key_validity(api_key_id): - access_token = request.cookies.get('stack-access-token') + access_token = request.cookies.get('hexclave-access-token') if not access_token: return jsonify({'error': 'Not authenticated'}), 401 @@ -1252,10 +1303,10 @@ You can check if an API key is still valid: response = requests.get( f'https://api.hexclave.com/api/v1/user-api-keys/{api_key_id}', headers={ - 'x-stack-access-type': 'client', - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-access-token': access_token, + '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, } ) @@ -1283,3 +1334,170 @@ You can check if an API key is still valid: ``` + +### 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" + "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" + 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" + "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" + 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" + 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" + 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. From 98172be761d9306c3e4afa024a899c0b4e6c303b Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 10:15:36 -0500 Subject: [PATCH 09/40] data-vault updates --- .../guides/apps/data-vault/overview.mdx | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/docs-mintlify/guides/apps/data-vault/overview.mdx b/docs-mintlify/guides/apps/data-vault/overview.mdx index 5889f4c53..c4b1ddcfd 100644 --- a/docs-mintlify/guides/apps/data-vault/overview.mdx +++ b/docs-mintlify/guides/apps/data-vault/overview.mdx @@ -21,16 +21,44 @@ Because keys are hashed before storage, **you cannot list or enumerate keys** in ## 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** page in the [Hexclave dashboard](https://app.hexclave.com) and create a new store. Each store has a unique ID that you'll reference in your code. +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" -STACK_DATA_VAULT_SECRET=your-randomly-generated-secret-here +HEXCLAVE_DATA_VAULT_SECRET=your-randomly-generated-secret-here ``` ### 3. Use the SDK @@ -38,18 +66,20 @@ STACK_DATA_VAULT_SECRET=your-randomly-generated-secret-here Data Vault is accessed through the **server app** only — it requires your secret server key. ```typescript title="server-example.ts" +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.STACK_DATA_VAULT_SECRET, + secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); // Retrieve a value const value = await store.getValue(key, { - secret: process.env.STACK_DATA_VAULT_SECRET, + secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); // value is the decrypted string, or null if the key doesn't exist ``` @@ -70,7 +100,7 @@ Retrieves the decrypted value for the given key, or `null` if the key doesn't ex ```typescript const value = await store.getValue("some-key", { - secret: process.env.STACK_DATA_VAULT_SECRET, + secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); ``` @@ -80,7 +110,7 @@ Stores an encrypted value for the given key. If the key already exists, it is ov ```typescript await store.setValue("some-key", "some-value", { - secret: process.env.STACK_DATA_VAULT_SECRET, + secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); ``` From 28ea1ba9d2b51e5dc701dc93387caa04f4830aeb Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 10:41:04 -0500 Subject: [PATCH 10/40] payments overview marketing page, and guide --- docs-mintlify/docs.json | 9 +- docs-mintlify/guides/apps/payments/guide.mdx | 417 +++++++++++++++++ .../guides/apps/payments/overview.mdx | 442 +++--------------- .../snippets/skeletons/payments-skeletons.jsx | 165 +++++++ 4 files changed, 657 insertions(+), 376 deletions(-) create mode 100644 docs-mintlify/guides/apps/payments/guide.mdx create mode 100644 docs-mintlify/snippets/skeletons/payments-skeletons.jsx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index b3dfd9ae7..1ea000823 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -126,7 +126,14 @@ "guides/apps/emails/templates-and-themes" ] }, - "guides/apps/payments/overview", + { + "group": "Payments", + "icon": "/images/app-icons/payments.svg", + "pages": [ + "guides/apps/payments/overview", + "guides/apps/payments/guide" + ] + }, { "group": "Analytics", "icon": "/images/app-icons/analytics.svg", diff --git a/docs-mintlify/guides/apps/payments/guide.mdx b/docs-mintlify/guides/apps/payments/guide.mdx new file mode 100644 index 000000000..4de6f385f --- /dev/null +++ b/docs-mintlify/guides/apps/payments/guide.mdx @@ -0,0 +1,417 @@ +--- +title: "Payments" +description: "Accept payments and manage billing with Hexclave's Payments app" +sidebarTitle: "Guide" +--- + +import { PaymentsConcepts } from "/snippets/payments-concepts.jsx"; + +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. + +This guide walks through how to set up payments, sell products, manage subscriptions, and work with item-based entitlements like credits or seats. + +## 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. Support for other countries 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, in multiple currencies), 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 + +## Defining products + +Configure your products in **Payments -> Products & Items**. Each product has: + +- **Display name** - What the customer sees +- **Customer type** - Whether this product is for users, teams, or custom customers +- **Prices** - One or more prices, each with a currency amount and an optional billing interval (day, week, month, or year). Currency amounts are **decimal strings** like `"9.99"` or `"1000"` — not cent integers. Supported currencies: USD, EUR, GBP, JPY, INR, AUD, CAD. +- **Included items** - Items granted when the product is purchased, with configurable quantity, repeat schedule, and expiration behavior + +A few additional options: + +- **Product lines** - Assign products to a product line to make them mutually exclusive (e.g. plan tiers). Configure lines in **Payments -> Product Lines**. +- **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one. +- **Free trial** - Give customers a trial period before charging. Can be set on the product or on individual prices. +- **Server-only** - Hide the product 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). + +## Selling a product + +To sell a product, generate a checkout URL and redirect the user to it. The `createCheckoutUrl` method is available on both user and team objects. + + + + ```typescript title="app/components/purchase-button.tsx" + "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" + import { hexclaveServerApp } from "@/stack/server"; + + export default async function PurchasePage() { + const user = await hexclaveServerApp.getUser({ or: 'redirect' }); + + const checkoutUrl = await user.createCheckoutUrl({ + productId: "prod_premium_monthly", + }); + + return Upgrade to Premium; + } + ``` + + + +For team purchases, call `createCheckoutUrl` on the team object instead: + +```typescript +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. + + +## Checking what a customer owns + +After a purchase, you'll want to know what the customer has. There are two ways to do this: check their product list, or check a specific item balance. + +### Listing products + +```typescript +// 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) +- `type` - `"one_time"` or `"subscription"` +- `subscription` - Subscription details (period end, cancel state) if applicable +- `switchOptions` - Other products in the same product line the customer could switch to + +### Checking item balances + +Items are the building blocks of entitlements. When a product includes items like "100 credits" or "5 seats", those are tracked as item balances on the customer. + +```typescript +// 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" +"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 atomic and race-condition-safe - it will return `false` and do nothing if the balance would go negative. + +```typescript title="lib/credits.ts" +import { hexclaveServerApp } from "@/stack/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)`. + +## Managing subscriptions + +### Switching plans + +When products belong to the same product line, customers can switch between them. For example, upgrading from Pro to Enterprise: + +```typescript title="app/components/upgrade-button.tsx" +"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. The switch happens immediately and the charge is prorated automatically. + + +### Canceling a subscription + +`cancelSubscription` is called on the app instance rather than the user object: + + + + ```typescript + "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 + // 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", + }); + ``` + + + +## Billing and payment methods + +Customers can save a payment method for future purchases and plan switches. This is built on a secure setup-intent flow. + +```typescript +// Create a setup intent +const setupIntent = await user.createPaymentMethodSetupIntent(); +// setupIntent.clientSecret - use with the embedded card form to collect card details +// setupIntent.stripeAccountId - the connected payment account ID + +// After the user completes the card form: +const paymentMethod = await user.setDefaultPaymentMethodFromSetupIntent( + setupIntentId +); +// paymentMethod contains: id, brand, last4, exp_month, exp_year +``` + +To check if a customer has a payment method saved: + +```typescript +// 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 +// Client component (hook) +const invoices = user.useInvoices(); + +// Server component +const invoices = await user.listInvoices(); +``` + +Each invoice includes `createdAt`, `status` (`"draft"`, `"open"`, `"paid"`, `"uncollectible"`, `"void"`), `amountTotal` (in cents), and `hostedInvoiceUrl` (a link to the hosted invoice page). + +Invoices support pagination: + +```typescript +const firstPage = await user.listInvoices({ limit: 10 }); +const secondPage = await user.listInvoices({ + limit: 10, + cursor: firstPage.nextCursor, +}); +``` + + + Invoices are available for user and team customers only, not custom customers. + + +## Granting products programmatically + +Sometimes you need to give a customer a product without going through checkout - free trials, admin grants, promotional offers, etc. Use `grantProduct` on the server: + +```typescript +// Grant a pre-configured product to a user +await hexclaveServerApp.grantProduct({ + userId: "user-id", + productId: "prod_premium", +}); + +// 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", +}); +``` + +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 +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 }, + }, + }, +}); +``` + +If you have a reference to the user object already, you can also call `user.grantProduct({ productId })` directly. + +## Customer types + +Hexclave supports three types of payment customers: + +- **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. Custom customers can only be managed via the server SDK and do not support billing or invoices. + +All payment methods (`createCheckoutUrl`, `useProducts`, `useItem`, `switchSubscription`, `useBilling`, `useInvoices`, etc.) are available on both user and team objects. For custom customers, use the top-level `hexclaveServerApp` methods with `customCustomerId`. + +## Dashboard + +The dashboard gives you full visibility and control over your payments: + +- **Product Lines** - Group products into mutually exclusive tiers. Configure 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 (with optional expiration), and grant products directly. In **Payments -> Customers**. +- **Transactions** - See all payment activity, filter by type and customer, view details, and issue refunds. In **Payments -> Transactions**. +- **Payouts** - View payout information. In **Payments -> Payouts**. +- **Settings** - Connect your payment account, toggle test mode, configure payment methods, and block new purchases. 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 + +These apply to both one-time purchases and subscription renewals. Customize them in **Emails -> Templates**. + +## Test mode + +During development, enable test mode in **Payments -> Settings**. All purchases will be free and no real money is charged - products are granted immediately without going through the payment processor. + +When you're ready to test with real payment flows (but still using test credentials), disable Hexclave's test mode and use these test card numbers: + +- **Success**: `4242 4242 4242 4242` +- **Decline**: `4000 0000 0000 0002` +- **Insufficient Funds**: `4000 0000 0000 9995` diff --git a/docs-mintlify/guides/apps/payments/overview.mdx b/docs-mintlify/guides/apps/payments/overview.mdx index 1e5c7562b..52cd563fa 100644 --- a/docs-mintlify/guides/apps/payments/overview.mdx +++ b/docs-mintlify/guides/apps/payments/overview.mdx @@ -1,419 +1,111 @@ --- title: "Payments" -description: "Accept payments and manage billing with Hexclave's Stripe integration" -icon: "/images/app-icons/payments.svg" +description: "Sell subscriptions and one-time products, and manage entitlements like credits and seats - billing without the plumbing" +sidebarTitle: "Overview" --- -import { PaymentsConcepts } from "/snippets/payments-concepts.jsx"; + + + **For agents/LLMs:** This is a high-level *marketing* overview of the Payments app, not an implementation reference. To actually build with payments, use [Setup](/guides/getting-started/setup) and the [Payments guide](./guide) (products, checkout, items/entitlements, subscriptions, billing, invoices, and granting products). + + -Hexclave includes a Payments app that handles billing, subscriptions, and one-time purchases through Stripe. 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. +import { PricingSkeleton, EntitlementsSkeleton, TransactionsSkeleton } from "/snippets/skeletons/payments-skeletons.jsx"; -This guide walks through how to set up payments, sell products, manage subscriptions, and work with item-based entitlements like credits or seats. +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. -## Getting started +## Can I sell a subscription or one-time product? - - - Go to the **Apps** section in your dashboard, find **Payments**, and enable it. - - - Open **Payments -> Settings** and follow the Stripe Connect 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. Support for other countries 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, in multiple currencies), 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 Stripe -3. The user pays, Stripe notifies Hexclave, and the product is granted -4. Your app reads the customer's products and item balances to control access - -## Defining products - -Configure your products in **Payments -> Products & Items**. Each product has: - -- **Display name** - What the customer sees -- **Customer type** - Whether this product is for users, teams, or custom customers -- **Prices** - One or more prices, each with a currency amount and an optional billing interval (day, week, month, or year). Currency amounts are **decimal strings** like `"9.99"` or `"1000"` — not cent integers. Supported currencies: USD, EUR, GBP, JPY, INR, AUD, CAD. -- **Included items** - Items granted when the product is purchased, with configurable quantity, repeat schedule, and expiration behavior - -A few additional options: - -- **Product lines** - Assign products to a product line to make them mutually exclusive (e.g. plan tiers). Configure lines in **Payments -> Product Lines**. -- **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one. -- **Free trial** - Give customers a trial period before charging. Can be set on the product or on individual prices. -- **Server-only** - Hide the product 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). - -## Selling a product - -To sell a product, generate a checkout URL and redirect the user to it. The `createCheckoutUrl` method is available on both user and team objects. - - - - ```typescript title="app/components/purchase-button.tsx" - "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" - import { hexclaveServerApp } from "@/stack/server"; - - export default async function PurchasePage() { - const user = await hexclaveServerApp.getUser({ or: 'redirect' }); - - const checkoutUrl = await user.createCheckoutUrl({ - productId: "prod_premium_monthly", - }); - - return Upgrade to Premium; - } - ``` - - - -For team purchases, call `createCheckoutUrl` on the team object instead: +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 -const team = user.useTeam(teamId); -const checkoutUrl = await team.createCheckoutUrl({ productId }); +const checkoutUrl = await user.createCheckoutUrl({ + productId: "pro_monthly", + returnUrl: window.location.href, +}); +window.location.href = checkoutUrl; ``` - - 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. - + -## Checking what a customer owns +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. -After a purchase, you'll want to know what the customer has. There are two ways to do this: check their product list, or check a specific item balance. +## Can I sell credits, seats, or API quota? -### Listing products +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 -// 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) -- `type` - `"one_time"` or `"subscription"` -- `subscription` - Subscription details (period end, cancel state) if applicable -- `switchOptions` - Other products in the same product line the customer could switch to - -### Checking item balances - -Items are the building blocks of entitlements. When a product includes items like "100 credits" or "5 seats", those are tracked as item balances on the customer. - -```typescript -// Client component (hook - re-renders on changes) +// Client - re-renders when the balance changes const credits = user.useItem("credits"); +return {credits.nonNegativeQuantity} credits left; +``` -// Server component +## 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 const credits = await user.getItem("credits"); +const ok = await credits.tryDecreaseQuantity(1); +if (!ok) throw new Error("Out of credits"); +// ...do the work ``` -An item has two quantity fields: +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. -- `quantity` - The raw balance (can be negative if you've consumed more than granted) -- `nonNegativeQuantity` - `Math.max(0, quantity)` for display purposes +## What exactly does Hexclave own for me? -Here's a practical example - showing a credits counter: +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: -```typescript title="app/components/credits-widget.tsx" -"use client"; -import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package +- **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, with entitlements revoked in step +- **Sync** - entitlement and subscription state stays consistent without a database to babysit -export default function CreditsWidget() { - const user = useUser({ or: 'redirect' }); - const credits = user.useItem("credits"); + - return ( -
-

Available Credits

-

{credits.nonNegativeQuantity}

-
- ); -} -``` +## Can I bill teams and external customers, not just users? -### Consuming credits (server-side) +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. -When your app needs to consume credits (e.g. when a user sends an AI request), use `tryDecreaseQuantity` on the server. It's atomic and race-condition-safe - it will return `false` and do nothing if the balance would go negative. +## Can I manage subscriptions in code? -```typescript title="lib/credits.ts" -import { hexclaveServerApp } from "@/stack/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)`. - -## Managing subscriptions - -### Switching plans - -When products belong to the same product line, customers can switch between them. For example, upgrading from Pro to Enterprise: - -```typescript title="app/components/upgrade-button.tsx" -"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. The switch happens immediately and Stripe prorates the charge automatically. - - -### Canceling a subscription - -`cancelSubscription` is called on the app instance rather than the user object: - - - - ```typescript - "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 - // 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", - }); - ``` - - - -## Billing and payment methods - -Customers can save a payment method for future purchases and plan switches. This is built on Stripe's SetupIntents. +Yes. Switch a customer between plans in the same product line, or cancel, with a single call - proration and timing handled for you. ```typescript -// Create a setup intent -const setupIntent = await user.createPaymentMethodSetupIntent(); -// setupIntent.clientSecret - use with Stripe Elements to collect card details -// setupIntent.stripeAccountId - the connected Stripe account ID - -// After the user completes Stripe's card form: -const paymentMethod = await user.setDefaultPaymentMethodFromSetupIntent( - setupIntentId -); -// paymentMethod contains: id, brand, last4, exp_month, exp_year -``` - -To check if a customer has a payment method saved: - -```typescript -// Client component (hook) -const billing = user.useBilling(); - -// Server component -const billing = await user.getBilling(); - -// billing.hasCustomer - whether a Stripe customer exists -// billing.defaultPaymentMethod - card details or null -``` - -## Invoices - -List a customer's invoices for displaying billing history: - -```typescript -// Client component (hook) -const invoices = user.useInvoices(); - -// Server component -const invoices = await user.listInvoices(); -``` - -Each invoice includes `createdAt`, `status` (`"draft"`, `"open"`, `"paid"`, `"uncollectible"`, `"void"`), `amountTotal` (in cents), and `hostedInvoiceUrl` (a link to Stripe's hosted invoice page). - -Invoices support pagination: - -```typescript -const firstPage = await user.listInvoices({ limit: 10 }); -const secondPage = await user.listInvoices({ - limit: 10, - cursor: firstPage.nextCursor, -}); -``` - - - Invoices are available for user and team customers only, not custom customers. - - -## Granting products programmatically - -Sometimes you need to give a customer a product without going through checkout - free trials, admin grants, promotional offers, etc. Use `grantProduct` on the server: - -```typescript -// Grant a pre-configured product to a user -await hexclaveServerApp.grantProduct({ - userId: "user-id", - productId: "prod_premium", +await user.switchSubscription({ + fromProductId: "pro_monthly", + toProductId: "enterprise_monthly", }); -// 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", -}); +await hexclaveServerApp.cancelSubscription({ productId: "pro_monthly" }); ``` -You can also grant products with an **inline definition** - no pre-configured product needed. This is useful for one-off grants like bonus credits: +You can also `grantProduct` directly - for trials, comps, or admin grants - with a pre-defined product or an inline one-off definition. -```typescript -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 }, - }, - }, -}); -``` +## Can I build and test before going live? -If you have a reference to the user object already, you can also call `user.grantProduct({ productId })` directly. +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. -## Customer types +## What it costs, and what it doesn't do -Hexclave supports three types of payment customers: +Straight answers so there are no surprises: -- **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. Custom customers can only be managed via the server SDK and do not support billing or invoices. +- **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. -All payment methods (`createCheckoutUrl`, `useProducts`, `useItem`, `switchSubscription`, `useBilling`, `useInvoices`, etc.) are available on both user and team objects. For custom customers, use the top-level `hexclaveServerApp` methods with `customCustomerId`. +## Start here -## Dashboard +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(...)`. -The dashboard gives you full visibility and control over your payments: - -- **Product Lines** - Group products into mutually exclusive tiers. Configure 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 (with optional expiration), and grant products directly. In **Payments -> Customers**. -- **Transactions** - See all payment activity, filter by type and customer, view details, and issue refunds. In **Payments -> Transactions**. -- **Payouts** - View payout information. In **Payments -> Payouts**. -- **Settings** - Connect Stripe, toggle test mode, configure payment methods, and block new purchases. 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 - -These apply to both one-time purchases and subscription renewals. Customize them in **Emails -> Templates**. - -## Test mode - -During development, enable test mode in **Payments -> Settings**. All purchases will be free and no real money is charged - products are granted immediately without going through Stripe. - -When you're ready to test with real Stripe flows (but still using test credentials), disable Stack's test mode and use Stripe's test card numbers: - -- **Success**: `4242 4242 4242 4242` -- **Decline**: `4000 0000 0000 0002` -- **Insufficient Funds**: `4000 0000 0000 9995` - -See [Stripe's testing documentation](https://stripe.com/docs/testing) for more test scenarios. +Ready for the details - products, items, subscriptions, billing, invoices, and grants? Read the [Payments guide](./guide). diff --git a/docs-mintlify/snippets/skeletons/payments-skeletons.jsx b/docs-mintlify/snippets/skeletons/payments-skeletons.jsx new file mode 100644 index 000000000..6d95c04a4 --- /dev/null +++ b/docs-mintlify/snippets/skeletons/payments-skeletons.jsx @@ -0,0 +1,165 @@ +// Lightweight, decorative skeletons used to illustrate the Payments app. +// They are intentionally non-interactive and use neutral placeholders so they +// read as "examples" rather than live UI. +// +// NOTE: Mintlify evaluates each exported component in isolation, so every +// component must be fully self-contained — no shared module-level constants or +// helper components. That's why ACCENT / Frame are redefined inside each one. + +export const PricingSkeleton = () => { + const ACCENT = "#10b981"; + + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const tier = (name, price, highlighted) => ( +
+
+ {name} + {highlighted && ( + + Popular + + )} +
+
+ {price} + /mo +
+
+ {["80%", "65%", "72%"].map((w, i) => ( +
+
+
+
+ ))} +
+
+ ); + + return ( +
+ +
+ {tier("Free", "$0", false)} + {tier("Pro", "$20", true)} + {tier("Enterprise", "$99", false)} +
+ +
+ ); +}; + +export const EntitlementsSkeleton = () => { + const ACCENT = "#10b981"; + + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const meter = (label, value, pct) => ( +
+
+ {label} + {value} +
+
+
+
+
+ ); + + return ( +
+ +
+ {meter("Credits", "820", "82%")} + {meter("Seats", "4 / 5", "80%")} + {meter("API quota", "12k", "47%")} +
+ +
+ ); +}; + +export const TransactionsSkeleton = () => { + const Frame = ({ label, children }) => ( +
+
+
+
+
+
+
+ {label} +
+
{children}
+
+ ); + + const row = (amount, status, color) => ( +
+
+
+
+
+
+
+
+
+ {amount} + + {status} + +
+
+ ); + + return ( +
+ +
+ {row("$20.00", "Paid", "#10b981")} + {row("$99.00", "Renewed", "#10b981")} + {row("$20.00", "Refunded", "#f59e0b")} + {row("$20.00", "Failed", "#ef4444")} +
+ +
+ ); +}; From 987692ca60f9fc879a0f5221f463313291302f4f Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 12:06:54 -0500 Subject: [PATCH 11/40] Update payments into concept documents --- docs-mintlify/docs.json | 13 +- .../apps/payments/billing-and-invoices.mdx | 67 +++ .../guides/apps/payments/checkout.mdx | 85 ++++ .../guides/apps/payments/customers.mdx | 18 + .../apps/payments/granting-products.mdx | 57 +++ docs-mintlify/guides/apps/payments/guide.mdx | 417 ------------------ .../apps/payments/items-and-entitlements.mdx | 83 ++++ .../guides/apps/payments/overview.mdx | 4 +- .../apps/payments/products-and-pricing.mdx | 37 ++ docs-mintlify/guides/apps/payments/setup.mdx | 87 ++++ .../guides/apps/payments/subscriptions.mdx | 79 ++++ 11 files changed, 527 insertions(+), 420 deletions(-) create mode 100644 docs-mintlify/guides/apps/payments/billing-and-invoices.mdx create mode 100644 docs-mintlify/guides/apps/payments/checkout.mdx create mode 100644 docs-mintlify/guides/apps/payments/customers.mdx create mode 100644 docs-mintlify/guides/apps/payments/granting-products.mdx delete mode 100644 docs-mintlify/guides/apps/payments/guide.mdx create mode 100644 docs-mintlify/guides/apps/payments/items-and-entitlements.mdx create mode 100644 docs-mintlify/guides/apps/payments/products-and-pricing.mdx create mode 100644 docs-mintlify/guides/apps/payments/setup.mdx create mode 100644 docs-mintlify/guides/apps/payments/subscriptions.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 1ea000823..4130accc9 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -131,7 +131,14 @@ "icon": "/images/app-icons/payments.svg", "pages": [ "guides/apps/payments/overview", - "guides/apps/payments/guide" + "guides/apps/payments/setup", + "guides/apps/payments/products-and-pricing", + "guides/apps/payments/items-and-entitlements", + "guides/apps/payments/checkout", + "guides/apps/payments/subscriptions", + "guides/apps/payments/billing-and-invoices", + "guides/apps/payments/granting-products", + "guides/apps/payments/customers" ] }, { @@ -331,6 +338,10 @@ { "source": "/guides/apps/fraud-protection/overview", "destination": "/guides/apps/authentication/fraud-protection" + }, + { + "source": "/guides/apps/payments/guide", + "destination": "/guides/apps/payments/setup" } ] } diff --git a/docs-mintlify/guides/apps/payments/billing-and-invoices.mdx b/docs-mintlify/guides/apps/payments/billing-and-invoices.mdx new file mode 100644 index 000000000..a2574e08b --- /dev/null +++ b/docs-mintlify/guides/apps/payments/billing-and-invoices.mdx @@ -0,0 +1,67 @@ +--- +title: "Billing & Invoices" +description: "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 +// 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 +// 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 +// 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 +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)). + + +## Related + +- [Subscriptions](./subscriptions) - switching plans requires a saved payment method +- [Checkout & Purchases](./checkout) - sell a product diff --git a/docs-mintlify/guides/apps/payments/checkout.mdx b/docs-mintlify/guides/apps/payments/checkout.mdx new file mode 100644 index 000000000..cd3b74c8e --- /dev/null +++ b/docs-mintlify/guides/apps/payments/checkout.mdx @@ -0,0 +1,85 @@ +--- +title: "Checkout & Purchases" +description: "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" + "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" + import { hexclaveServerApp } from "@/stack/server"; + + export default async function PurchasePage() { + const user = await hexclaveServerApp.getUser({ or: 'redirect' }); + + const checkoutUrl = await user.createCheckoutUrl({ + productId: "prod_premium_monthly", + }); + + return Upgrade to Premium; + } + ``` + + + +For team purchases, call `createCheckoutUrl` on the team object instead: + +```typescript +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. + + +## 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 +// 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 +- [Granting Products](./granting-products) - give access without checkout diff --git a/docs-mintlify/guides/apps/payments/customers.mdx b/docs-mintlify/guides/apps/payments/customers.mdx new file mode 100644 index 000000000..61d5d93c5 --- /dev/null +++ b/docs-mintlify/guides/apps/payments/customers.mdx @@ -0,0 +1,18 @@ +--- +title: "Customer Types" +description: "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. Custom customers are managed only via the server SDK and do not support billing or invoices. + +## 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 [`grantProduct`](./granting-products), `getItem`, and `listProducts`). Billing, invoices, 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. diff --git a/docs-mintlify/guides/apps/payments/granting-products.mdx b/docs-mintlify/guides/apps/payments/granting-products.mdx new file mode 100644 index 000000000..1a267b165 --- /dev/null +++ b/docs-mintlify/guides/apps/payments/granting-products.mdx @@ -0,0 +1,57 @@ +--- +title: "Granting Products" +description: "Give a customer a product without going through checkout" +--- + +Sometimes you need to give a customer a product without charging them - free trials, admin grants, promotional offers, support credits. Use `grantProduct` on the server. + +## Granting a configured product + +```typescript +// Grant a pre-configured product to a user +await hexclaveServerApp.grantProduct({ + userId: "user-id", + productId: "prod_premium", +}); + +// 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 })` 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 +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 +- [Customer Types](./customers) - grant to users, teams, or custom customers diff --git a/docs-mintlify/guides/apps/payments/guide.mdx b/docs-mintlify/guides/apps/payments/guide.mdx deleted file mode 100644 index 4de6f385f..000000000 --- a/docs-mintlify/guides/apps/payments/guide.mdx +++ /dev/null @@ -1,417 +0,0 @@ ---- -title: "Payments" -description: "Accept payments and manage billing with Hexclave's Payments app" -sidebarTitle: "Guide" ---- - -import { PaymentsConcepts } from "/snippets/payments-concepts.jsx"; - -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. - -This guide walks through how to set up payments, sell products, manage subscriptions, and work with item-based entitlements like credits or seats. - -## 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. Support for other countries 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, in multiple currencies), 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 - -## Defining products - -Configure your products in **Payments -> Products & Items**. Each product has: - -- **Display name** - What the customer sees -- **Customer type** - Whether this product is for users, teams, or custom customers -- **Prices** - One or more prices, each with a currency amount and an optional billing interval (day, week, month, or year). Currency amounts are **decimal strings** like `"9.99"` or `"1000"` — not cent integers. Supported currencies: USD, EUR, GBP, JPY, INR, AUD, CAD. -- **Included items** - Items granted when the product is purchased, with configurable quantity, repeat schedule, and expiration behavior - -A few additional options: - -- **Product lines** - Assign products to a product line to make them mutually exclusive (e.g. plan tiers). Configure lines in **Payments -> Product Lines**. -- **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one. -- **Free trial** - Give customers a trial period before charging. Can be set on the product or on individual prices. -- **Server-only** - Hide the product 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). - -## Selling a product - -To sell a product, generate a checkout URL and redirect the user to it. The `createCheckoutUrl` method is available on both user and team objects. - - - - ```typescript title="app/components/purchase-button.tsx" - "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" - import { hexclaveServerApp } from "@/stack/server"; - - export default async function PurchasePage() { - const user = await hexclaveServerApp.getUser({ or: 'redirect' }); - - const checkoutUrl = await user.createCheckoutUrl({ - productId: "prod_premium_monthly", - }); - - return Upgrade to Premium; - } - ``` - - - -For team purchases, call `createCheckoutUrl` on the team object instead: - -```typescript -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. - - -## Checking what a customer owns - -After a purchase, you'll want to know what the customer has. There are two ways to do this: check their product list, or check a specific item balance. - -### Listing products - -```typescript -// 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) -- `type` - `"one_time"` or `"subscription"` -- `subscription` - Subscription details (period end, cancel state) if applicable -- `switchOptions` - Other products in the same product line the customer could switch to - -### Checking item balances - -Items are the building blocks of entitlements. When a product includes items like "100 credits" or "5 seats", those are tracked as item balances on the customer. - -```typescript -// 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" -"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 atomic and race-condition-safe - it will return `false` and do nothing if the balance would go negative. - -```typescript title="lib/credits.ts" -import { hexclaveServerApp } from "@/stack/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)`. - -## Managing subscriptions - -### Switching plans - -When products belong to the same product line, customers can switch between them. For example, upgrading from Pro to Enterprise: - -```typescript title="app/components/upgrade-button.tsx" -"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. The switch happens immediately and the charge is prorated automatically. - - -### Canceling a subscription - -`cancelSubscription` is called on the app instance rather than the user object: - - - - ```typescript - "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 - // 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", - }); - ``` - - - -## Billing and payment methods - -Customers can save a payment method for future purchases and plan switches. This is built on a secure setup-intent flow. - -```typescript -// Create a setup intent -const setupIntent = await user.createPaymentMethodSetupIntent(); -// setupIntent.clientSecret - use with the embedded card form to collect card details -// setupIntent.stripeAccountId - the connected payment account ID - -// After the user completes the card form: -const paymentMethod = await user.setDefaultPaymentMethodFromSetupIntent( - setupIntentId -); -// paymentMethod contains: id, brand, last4, exp_month, exp_year -``` - -To check if a customer has a payment method saved: - -```typescript -// 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 -// Client component (hook) -const invoices = user.useInvoices(); - -// Server component -const invoices = await user.listInvoices(); -``` - -Each invoice includes `createdAt`, `status` (`"draft"`, `"open"`, `"paid"`, `"uncollectible"`, `"void"`), `amountTotal` (in cents), and `hostedInvoiceUrl` (a link to the hosted invoice page). - -Invoices support pagination: - -```typescript -const firstPage = await user.listInvoices({ limit: 10 }); -const secondPage = await user.listInvoices({ - limit: 10, - cursor: firstPage.nextCursor, -}); -``` - - - Invoices are available for user and team customers only, not custom customers. - - -## Granting products programmatically - -Sometimes you need to give a customer a product without going through checkout - free trials, admin grants, promotional offers, etc. Use `grantProduct` on the server: - -```typescript -// Grant a pre-configured product to a user -await hexclaveServerApp.grantProduct({ - userId: "user-id", - productId: "prod_premium", -}); - -// 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", -}); -``` - -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 -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 }, - }, - }, -}); -``` - -If you have a reference to the user object already, you can also call `user.grantProduct({ productId })` directly. - -## Customer types - -Hexclave supports three types of payment customers: - -- **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. Custom customers can only be managed via the server SDK and do not support billing or invoices. - -All payment methods (`createCheckoutUrl`, `useProducts`, `useItem`, `switchSubscription`, `useBilling`, `useInvoices`, etc.) are available on both user and team objects. For custom customers, use the top-level `hexclaveServerApp` methods with `customCustomerId`. - -## Dashboard - -The dashboard gives you full visibility and control over your payments: - -- **Product Lines** - Group products into mutually exclusive tiers. Configure 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 (with optional expiration), and grant products directly. In **Payments -> Customers**. -- **Transactions** - See all payment activity, filter by type and customer, view details, and issue refunds. In **Payments -> Transactions**. -- **Payouts** - View payout information. In **Payments -> Payouts**. -- **Settings** - Connect your payment account, toggle test mode, configure payment methods, and block new purchases. 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 - -These apply to both one-time purchases and subscription renewals. Customize them in **Emails -> Templates**. - -## Test mode - -During development, enable test mode in **Payments -> Settings**. All purchases will be free and no real money is charged - products are granted immediately without going through the payment processor. - -When you're ready to test with real payment flows (but still using test credentials), disable Hexclave's test mode and use these test card numbers: - -- **Success**: `4242 4242 4242 4242` -- **Decline**: `4000 0000 0000 0002` -- **Insufficient Funds**: `4000 0000 0000 9995` diff --git a/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx b/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx new file mode 100644 index 000000000..204d83f12 --- /dev/null +++ b/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx @@ -0,0 +1,83 @@ +--- +title: "Items & Entitlements" +description: "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 +// 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" +"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" +import { hexclaveServerApp } from "@/stack/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 (with an optional expiration) - useful for support credits, comps, or corrections. + +## Related + +- [Products & Pricing](./products-and-pricing) - attach items to a product +- [Granting Products](./granting-products) - grant a one-off bundle of items diff --git a/docs-mintlify/guides/apps/payments/overview.mdx b/docs-mintlify/guides/apps/payments/overview.mdx index 52cd563fa..b646a3878 100644 --- a/docs-mintlify/guides/apps/payments/overview.mdx +++ b/docs-mintlify/guides/apps/payments/overview.mdx @@ -6,7 +6,7 @@ sidebarTitle: "Overview" - **For agents/LLMs:** This is a high-level *marketing* overview of the Payments app, not an implementation reference. To actually build with payments, use [Setup](/guides/getting-started/setup) and the [Payments guide](./guide) (products, checkout, items/entitlements, subscriptions, billing, invoices, and granting products). + **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), [Granting Products](./granting-products), and [Customer Types](./customers). @@ -108,4 +108,4 @@ Straight answers so there are no surprises: 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, and grants? Read the [Payments guide](./guide). +Ready for the details - products, items, subscriptions, billing, invoices, and grants? Start with [Payments setup](./setup). diff --git a/docs-mintlify/guides/apps/payments/products-and-pricing.mdx b/docs-mintlify/guides/apps/payments/products-and-pricing.mdx new file mode 100644 index 000000000..438cd35da --- /dev/null +++ b/docs-mintlify/guides/apps/payments/products-and-pricing.mdx @@ -0,0 +1,37 @@ +--- +title: "Products & Pricing" +description: "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**. + +## 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. Payments 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. Can be set on the product or on individual prices. +- **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one. +- **Server-only** - Hide the product 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). + +## 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. + + +## Related + +- [Items & Entitlements](./items-and-entitlements) - attach credits, seats, and quota to a product +- [Checkout & Purchases](./checkout) - sell a product once it's defined diff --git a/docs-mintlify/guides/apps/payments/setup.mdx b/docs-mintlify/guides/apps/payments/setup.mdx new file mode 100644 index 000000000..467fae733 --- /dev/null +++ b/docs-mintlify/guides/apps/payments/setup.mdx @@ -0,0 +1,87 @@ +--- +title: "Setup" +description: "Enable Payments, connect a payment account, and understand how the pieces fit together" +--- + +import { PaymentsConcepts } from "/snippets/payments-concepts.jsx"; + +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 +- [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**. + + + 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 (with optional expiration), and grant products directly. In **Payments -> Customers**. +- **Transactions** - See all payment activity, filter by type and customer, view details, and issue refunds. In **Payments -> Transactions**. +- **Payouts** - View payout information. In **Payments -> Payouts**. +- **Settings** - Connect your payment account, toggle test mode, configure payment methods, and block new purchases. 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 + +These apply to both one-time purchases and subscription renewals. Customize them in **Emails -> Templates** (see the [Emails guide](/guides/apps/emails/guide)). diff --git a/docs-mintlify/guides/apps/payments/subscriptions.mdx b/docs-mintlify/guides/apps/payments/subscriptions.mdx new file mode 100644 index 000000000..acbe57a3c --- /dev/null +++ b/docs-mintlify/guides/apps/payments/subscriptions.mdx @@ -0,0 +1,79 @@ +--- +title: "Subscriptions" +description: "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" +"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. + + +## Canceling a subscription + +`cancelSubscription` is called on the app instance rather than the user object: + + + + ```typescript + "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 + // 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", + }); + ``` + + + +## 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 From 854e3456bcf8e1702061ede1b2c41d02af8f66c3 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 12:08:10 -0500 Subject: [PATCH 12/40] Update launch checklist docs --- .../guides/apps/launch-checklist/overview.mdx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs-mintlify/guides/apps/launch-checklist/overview.mdx b/docs-mintlify/guides/apps/launch-checklist/overview.mdx index d78075e7e..c7e19033c 100644 --- a/docs-mintlify/guides/apps/launch-checklist/overview.mdx +++ b/docs-mintlify/guides/apps/launch-checklist/overview.mdx @@ -1,20 +1,20 @@ --- title: "Launch Checklist" -description: "Steps to prepare Stack for production use" +description: "Steps to prepare Hexclave for production use" icon: "/images/app-icons/launch-checklist.svg" --- -Stack 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: +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, Stack 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, Stack must know your domain (e.g., `https://your-website.com`) and only allow callbacks from those 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 Stack dashboard. If you haven't configured your handler, you can leave it as the default. (Learn more about handlers [here](/sdk/objects/hexclave-app)). + 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/stack-app)). For enhanced security, disable the `Allow all localhost callbacks for development` option. @@ -23,13 +23,13 @@ Follow these steps when you're ready to push your application to production: ## OAuth providers -Stack 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 "Stack 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 Stack. +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 Stack callback URL. Copy the client ID and client secret. + 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. @@ -116,19 +116,19 @@ To use your own OAuth provider setups in production, follow these steps for each - Go to the `Auth Methods` section in the Stack dashboard, open the provider's settings, switch from shared keys to custom keys, and enter the client ID and client secret. + 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, Stack uses a shared email server, which sends emails from Stack'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. +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 Stack: +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 Stack's documentation scope). -2. **Configure Stack's Email Settings**: Navigate to the `Emails` section in the Stack dashboard, click `Edit` in the `Email Server` section, switch from `Shared` to `Custom SMTP server`, enter your SMTP configurations, and save. +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 Stack dashboard, ensuring that your website runs securely with Stack in a production environment. +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. From 64e7d8cd00d3f3ef927b16a275bf029dddb8c454 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 19 Jun 2026 12:20:58 -0500 Subject: [PATCH 13/40] Updates for replays and clickmaps documentation --- docs-mintlify/docs.json | 3 +- .../guides/apps/analytics/overview.mdx | 15 +- .../apps/analytics/replays-and-clickmaps.mdx | 131 +++++++++++++ docs-mintlify/guides/apps/rbac/overview.mdx | 177 +++++++++++++++--- 4 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 4130accc9..339ceceff 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -146,7 +146,8 @@ "icon": "/images/app-icons/analytics.svg", "pages": [ "guides/apps/analytics/overview", - "guides/apps/analytics/queries-and-tables" + "guides/apps/analytics/queries-and-tables", + "guides/apps/analytics/replays-and-clickmaps" ] }, { diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index 9b543017e..cc60e42cc 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -1,18 +1,20 @@ --- title: "Analytics" description: "Explore events, session replays, and SQL queries in your project's analytics dataset" -icon: "/images/app-icons/analytics.svg" --- 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 three areas in the dashboard: +Analytics is organized into four areas in the dashboard: - **Tables**: Browse event rows 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**: Visualize where users click across your pages + +See [Replays & Clickmaps](./replays-and-clickmaps) for the full guide to session replays and clickmaps. ## How Analytics Works @@ -46,7 +48,7 @@ To use analytics in your project: After setup, Stack automatically captures client-side `$page-view` and `$click` events. -If you want replay recordings, also enable `analytics.replays.enabled` in your client app config. +Session replay recording is also on by default once Analytics is enabled. See [Replays & Clickmaps](./replays-and-clickmaps) to tune privacy or opt out. ## Tables @@ -81,9 +83,9 @@ The **Replays** screen helps you move from "an event happened" to "what the user Use replays when metrics alone are not enough to explain user behavior. -### Enabling Replay Recording in the SDK +### Replay recording in the SDK -Session replay recording is disabled by default. To enable it, pass `analytics.replays.enabled: true` when creating your client app. +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 import { HexclaveClientApp } from "@hexclave/js"; @@ -93,6 +95,7 @@ export const hexclaveClientApp = new HexclaveClientApp({ tokenStore: "cookie", // use "nextjs-cookie" in Next.js analytics: { replays: { + // Enabled by default; set to false to opt out. enabled: true, // Optional. Defaults to true. maskAllInputs: true, @@ -101,7 +104,7 @@ export const hexclaveClientApp = new HexclaveClientApp({ }); ``` -`maskAllInputs` defaults to `true`, so form fields are masked unless you explicitly disable it. +`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 diff --git a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx new file mode 100644 index 000000000..29e8d9ab0 --- /dev/null +++ b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx @@ -0,0 +1,131 @@ +--- +title: "Replays & Clickmaps" +description: "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. + + + Hexclave's spatial click visualization is called a **Clickmap** - it aggregates clicks per on-page element (with dead-click detection), rather than rendering a coordinate-density "heatmap." If you're looking for "heatmaps," this is the feature you want. + + +## 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). +3. The visitor has an **authenticated session**. Capture is tied to the user's session, so anonymous traffic isn't recorded. + +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." Recording is powered by [rrweb](https://www.rrweb.io/): the SDK captures DOM snapshots and mutations, mouse interactions, and page state over time, then plays them back as a faithful 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 +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 +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. 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 diff --git a/docs-mintlify/guides/apps/rbac/overview.mdx b/docs-mintlify/guides/apps/rbac/overview.mdx index bbb9c6a04..9cd73d04f 100644 --- a/docs-mintlify/guides/apps/rbac/overview.mdx +++ b/docs-mintlify/guides/apps/rbac/overview.mdx @@ -5,40 +5,109 @@ sidebarTitle: RBAC icon: "/images/app-icons/rbac.svg" --- -Permissions are a way to 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 -Stack supports two types of permissions: +Hexclave supports two types of permissions: 1. **Team Permissions**: Control what a user can do within a specific team -2. **User Permissions**: Control what a user can do globally, across the entire project +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 +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 Stack 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. +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 the `Team Permissions` section of the Stack dashboard. You can select the permissions that the new permission will contain. Any permissions included within these selected permissions will also be recursively included. +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 -Stack 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 Stack backend system. +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, use the `getPermission` method or the `usePermission` hook on the `User` object. This 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: +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" "use client"; - import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package + import { useUser } from "@hexclave/next"; export function CheckUserPermission() { const user = useUser({ or: 'redirect' }); @@ -56,7 +125,7 @@ To check whether a user has a specific permission, use the `getPermission` metho ```tsx title="Check user permission on the server" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CheckUserPermission() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); @@ -74,19 +143,38 @@ To check whether a user has a specific permission, use the `getPermission` metho +For authorization logic, prefer a boolean server-side check: + +```tsx title="app/api/team-settings/route.ts" +import { hexclaveServerApp } from "@/hexclave/server"; + +export async function POST() { + const user = await hexclaveServerApp.getUser({ or: "redirect" }); + 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, use the `listPermissions` method or the `usePermissions` hook on the `User` object. This method retrieves both direct and indirect permissions. Here is an example: +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" "use client"; - import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package + import { useUser } from "@hexclave/next"; export function DisplayUserPermissions() { const user = useUser({ or: 'redirect' }); - const permissions = user.usePermissions(); + const team = user.useTeam('some-team-id'); + const permissions = user.usePermissions(team); return (
@@ -100,11 +188,12 @@ To get a list of all permissions a user has, use the `listPermissions` method or ```tsx title="List user permissions on the server" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function DisplayUserPermissions() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); - const permissions = await user.listPermissions(); + const team = await hexclaveServerApp.getTeam('some-team-id'); + const permissions = team ? await user.listPermissions(team) : []; return (
@@ -125,6 +214,7 @@ To grant a permission to a user, use the `grantPermission` method on the `Server ```tsx 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'); ``` @@ -135,6 +225,7 @@ To revoke a permission from a user, use the `revokePermission` method on the `Se ```tsx 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'); ``` @@ -144,17 +235,17 @@ Project permissions are global permissions that apply to a user across the entir ### Creating a Project Permission -To create a new project permission, navigate to the `Project Permissions` section of the Stack dashboard. Similar to team permissions, you can select other permissions that the new permission will contain, creating a hierarchical structure. +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 the `getPermission` method or the `usePermission` hook. Here's an example: +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" "use client"; - import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package + import { useUser } from "@hexclave/next"; export function CheckGlobalPermission() { const user = useUser({ or: 'redirect' }); @@ -170,7 +261,7 @@ To check whether a user has a specific project permission, use the `getPermissio ```tsx title="Check user permission on the server" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function CheckGlobalPermission() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); @@ -186,15 +277,32 @@ To check whether a user has a specific project permission, use the `getPermissio +For authorization logic, prefer a server-side boolean check: + +```tsx title="app/admin/page.tsx" +import { hexclaveServerApp } from "@/hexclave/server"; + +export default async function AdminPage() { + const user = await hexclaveServerApp.getUser({ or: "redirect" }); + 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: +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" "use client"; - import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package + import { useUser } from "@hexclave/next"; export function DisplayGlobalPermissions() { const user = useUser({ or: 'redirect' }); @@ -212,7 +320,7 @@ To get a list of all global permissions a user has, use the `listPermissions` me ```tsx title="List global permissions on the server" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function DisplayGlobalPermissions() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); @@ -230,12 +338,19 @@ To get a list of all global permissions a user has, use the `listPermissions` me +If you only want direct project permission grants, pass `{ recursive: false }`: + +```tsx +const directPermissions = await user.listPermissions({ recursive: false }); +``` + ### Granting a Project Permission To grant a global permission to a user, use the `grantPermission` method: ```tsx const user = await hexclaveServerApp.getUser(); +if (!user) throw new Error("User not found"); await user.grantPermission('access_admin_dashboard'); ``` @@ -245,7 +360,25 @@ To revoke a global permission from a user, use the `revokePermission` method: ```tsx const user = await hexclaveServerApp.getUser(); +if (!user) throw new Error("User not found"); await user.revokePermission('access_admin_dashboard'); ``` -By following these guidelines, you can efficiently manage and verify both team and user permissions within your application. +## 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 +// 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. From bb87360c4f178fd586670a8161d6c32383d4d420 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 23 Jun 2026 14:33:00 -0500 Subject: [PATCH 14/40] custom-oidc --- docs-mintlify/docs.json | 5 +- .../apps/authentication/auth-providers.mdx | 12 ++- .../auth-providers/custom-oidc.mdx | 100 ++++++++++++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 docs-mintlify/guides/apps/authentication/auth-providers/custom-oidc.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 339ceceff..c60b2f214 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -108,11 +108,12 @@ "guides/apps/authentication/auth-providers/google", "guides/apps/authentication/auth-providers/linkedin", "guides/apps/authentication/auth-providers/microsoft", - "guides/apps/authentication/auth-providers/passkey", "guides/apps/authentication/auth-providers/spotify", "guides/apps/authentication/auth-providers/twitch", + "guides/apps/authentication/auth-providers/x-twitter", + "guides/apps/authentication/auth-providers/passkey", "guides/apps/authentication/auth-providers/two-factor-auth", - "guides/apps/authentication/auth-providers/x-twitter" + "guides/apps/authentication/auth-providers/custom-oidc" ] } ] diff --git a/docs-mintlify/guides/apps/authentication/auth-providers.mdx b/docs-mintlify/guides/apps/authentication/auth-providers.mdx index a171748f2..f68600c03 100644 --- a/docs-mintlify/guides/apps/authentication/auth-providers.mdx +++ b/docs-mintlify/guides/apps/authentication/auth-providers.mdx @@ -8,11 +8,12 @@ Hexclave supports a variety of authentication providers to give your users flexi ## Overview -Authentication providers determine how users can sign in to your application. Stack supports the following provider types: +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 @@ -28,14 +29,14 @@ Authentication providers determine how users can sign in to your application. St Toggle the providers you want to enable for your application. - For OAuth providers, you can use Stack's shared keys for development or configure your own OAuth client ID and client secret for production. + 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, Stack provides shared OAuth keys that work out of the box. For production, you should set up your own OAuth client credentials. + 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. ### Shared Keys @@ -93,6 +94,8 @@ For production use, configure your own OAuth client ID and client secret for eac ## Other Authentication Methods +These aren't OAuth providers, but round out how users sign in and secure their accounts. + WebAuthn-based passwordless authentication @@ -100,6 +103,9 @@ For production use, configure your own OAuth client ID and client secret for eac TOTP-based two-factor authentication + + Bring any OpenID Connect identity provider (Okta, Auth0, Keycloak, and more). Team plan or above. + diff --git a/docs-mintlify/guides/apps/authentication/auth-providers/custom-oidc.mdx b/docs-mintlify/guides/apps/authentication/auth-providers/custom-oidc.mdx new file mode 100644 index 000000000..e35c0a733 --- /dev/null +++ b/docs-mintlify/guides/apps/authentication/auth-providers/custom-oidc.mdx @@ -0,0 +1,100 @@ +--- +title: "Custom OIDC" +description: "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 + "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 +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) From 43ab087813ab44a32e8d6b3756a72d00343dcfd6 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:01:32 -0500 Subject: [PATCH 15/40] Update to uncouple auth <-> emails in emails overview --- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index d356d6469..3ae44bf48 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -12,7 +12,7 @@ sidebarTitle: "Overview" import { EmailPreviewSkeleton, EmailTemplatesSkeleton, DeliveryStatsSkeleton } from "/snippets/skeletons/email-skeletons.jsx"; -Email is the other half of auth - verification, password resets, receipts, product updates. The Emails app sends all of it from one server call, with templates, themes, scheduling, unsubscribe handling, and delivery tracking built in. Below are the questions developers actually ask, and the honest answers. +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? From 0467c3d752e9fb205c2f406a8860c0a511625e7d Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:02:52 -0500 Subject: [PATCH 16/40] common flows wording instead of auth flows wording on emails overview --- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 3ae44bf48..7fc65cbe7 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -48,7 +48,7 @@ export function EmailTemplate({ user, variables }: Props -Hexclave also ships ready-made templates for the common auth flows - email verification, password reset, magic link, team invitations, and payment receipts - wired up automatically and customizable from the dashboard. +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? From f5656e693415ca3d310c04f0c613ef784325f2ab Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:04:39 -0500 Subject: [PATCH 17/40] Add emails theme snippet in emails overview, linking to full templates and themes page --- docs-mintlify/guides/apps/emails/overview.mdx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 7fc65cbe7..2c5b60161 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -52,7 +52,23 @@ Hexclave also ships ready-made templates for the common flows - email verificati ## 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. Set a project default, override per-email with `themeId`, or send with no theme at all. +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 +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? From 3750949400d44aa48cbafec84e15ad1aa8f987ed Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:06:52 -0500 Subject: [PATCH 18/40] wording for managed domains is more clear, we do not manage DNS for you, rather the signing and deliverability --- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 2c5b60161..b3563b426 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -85,7 +85,7 @@ await hexclaveServerApp.sendEmail({ ## 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 and handle DNS and deliverability for you. 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. +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. ## Can I schedule and send in bulk? From 3914553a51dfbd1600a3f7fa393bc3be9ead24a4 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:10:24 -0500 Subject: [PATCH 19/40] More clear wording on what works in local development with hexclave for email --- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index b3563b426..39bfc9206 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -85,7 +85,7 @@ await hexclaveServerApp.sendEmail({ ## 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. +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? From 6f1e7964527e942b09573ac8f95f744d9e59c507 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:12:04 -0500 Subject: [PATCH 20/40] scheduledAt date moved to future date rather than past --- docs-mintlify/guides/apps/emails/guide.mdx | 4 ++-- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs-mintlify/guides/apps/emails/guide.mdx b/docs-mintlify/guides/apps/emails/guide.mdx index fd7734aad..fb1a62c6d 100644 --- a/docs-mintlify/guides/apps/emails/guide.mdx +++ b/docs-mintlify/guides/apps/emails/guide.mdx @@ -75,7 +75,7 @@ await hexclaveServerApp.sendEmail({ // themeId: null, // use the default theme // themeId: false, // send with no theme at all notificationCategoryName: 'Marketing', - scheduledAt: new Date('2025-12-25T00:00:00Z'), + scheduledAt: new Date('2027-12-25T00:00:00Z'), }); ``` @@ -139,7 +139,7 @@ await hexclaveServerApp.sendEmail({ userIds: ['user-id'], html: '

Happy New Year!

', subject: 'Happy New Year!', - scheduledAt: new Date('2026-01-01T00:00:00Z'), + scheduledAt: new Date('2027-01-01T00:00:00Z'), }); ``` diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 39bfc9206..e951c11c9 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -96,7 +96,7 @@ await hexclaveServerApp.sendEmail({ allUsers: true, templateId: "product-update", subject: "We just shipped a big update", - scheduledAt: new Date("2026-01-01T00:00:00Z"), + scheduledAt: new Date("2027-01-01T00:00:00Z"), }); ``` From b70f32e2957458d895d02a491f0ba07ce052e377 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:13:27 -0500 Subject: [PATCH 21/40] change drafts intro wording to be better --- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index e951c11c9..5ac271408 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -113,7 +113,7 @@ const info = await hexclaveServerApp.getEmailDeliveryStats(); ## Can I compose without writing code? -Yes. The dashboard has a full draft editor with live preview, theme selection, a recipient picker, and scheduling - then trigger the draft programmatically with `draftId` or send it straight from the dashboard. +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`. ## Start here From c00987ea4a5fe2e2f68195f015e9164cf1215516 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 02:22:51 -0500 Subject: [PATCH 22/40] Add drafts.mdx, and update references across emails docs. --- docs-mintlify/docs.json | 3 +- docs-mintlify/guides/apps/emails/drafts.mdx | 36 +++++++++++++++++++ docs-mintlify/guides/apps/emails/guide.mdx | 10 +----- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- .../apps/emails/templates-and-themes.mdx | 3 +- 5 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 docs-mintlify/guides/apps/emails/drafts.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 23217dfe9..949abbe36 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -124,7 +124,8 @@ "pages": [ "guides/apps/emails/overview", "guides/apps/emails/guide", - "guides/apps/emails/templates-and-themes" + "guides/apps/emails/templates-and-themes", + "guides/apps/emails/drafts" ] }, { diff --git a/docs-mintlify/guides/apps/emails/drafts.mdx b/docs-mintlify/guides/apps/emails/drafts.mdx new file mode 100644 index 000000000..4d02f08d9 --- /dev/null +++ b/docs-mintlify/guides/apps/emails/drafts.mdx @@ -0,0 +1,36 @@ +--- +title: "Drafts" +description: "Compose emails in the dashboard, then send from the UI or with draftId." +sidebarTitle: "Drafts" +--- + +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. Sending a draft (from the dashboard or from code) requires a custom email server — SMTP, Resend, or Managed — configured in the [cloud dashboard](https://app.hexclave.com). + +## Send a draft + +Send from the dashboard when you're ready, or trigger the same draft from your backend: + +```typescript +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. diff --git a/docs-mintlify/guides/apps/emails/guide.mdx b/docs-mintlify/guides/apps/emails/guide.mdx index fb1a62c6d..512c3f0ff 100644 --- a/docs-mintlify/guides/apps/emails/guide.mdx +++ b/docs-mintlify/guides/apps/emails/guide.mdx @@ -258,12 +258,4 @@ await hexclaveServerApp.activateEmailCapacityBoost(); ## Drafts -The dashboard includes a full draft editor where you can compose emails visually before sending. Drafts support: - -- TSX source editing with live preview -- Theme selection -- Recipient picker (specific users or all users) -- Scheduling -- Send history per draft - -Once a draft is sent, it's marked as sent and its delivery can be tracked in the outbox. +Compose emails visually in the dashboard under **Emails → Drafts**, then send from the UI or with `draftId`. See [Drafts](./drafts) for creating drafts, cloning from a template, and sending. diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 5ac271408..05df2fb45 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -113,7 +113,7 @@ const info = await hexclaveServerApp.getEmailDeliveryStats(); ## 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`. +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 diff --git a/docs-mintlify/guides/apps/emails/templates-and-themes.mdx b/docs-mintlify/guides/apps/emails/templates-and-themes.mdx index c162700c1..bd816d8e4 100644 --- a/docs-mintlify/guides/apps/emails/templates-and-themes.mdx +++ b/docs-mintlify/guides/apps/emails/templates-and-themes.mdx @@ -1,6 +1,6 @@ --- title: "Templates & Themes" -description: "Author email content with React Email templates and wrap it in reusable, branded themes." +description: "Build email bodies with React Email templates, then wrap them in reusable branded themes." sidebarTitle: "Templates & Themes" --- @@ -106,4 +106,5 @@ Set a default theme for your project in the dashboard. You can also override the ## Related +- [Drafts](./drafts) - compose in the dashboard and send with `draftId` (not TSX templates). - [Emails guide](./guide) - sending emails, scheduling, the delivery pipeline, and server configuration. From ebe7250d9fc663e854504eb2d4a0e10660782d7f Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:13:04 -0500 Subject: [PATCH 23/40] rework guide to be more of a start to finish guide/walkthrough --- docs-mintlify/guides/apps/emails/drafts.mdx | 2 +- docs-mintlify/guides/apps/emails/guide.mdx | 364 +++++++++--------- docs-mintlify/guides/apps/emails/overview.mdx | 2 +- .../apps/emails/templates-and-themes.mdx | 2 +- 4 files changed, 186 insertions(+), 184 deletions(-) diff --git a/docs-mintlify/guides/apps/emails/drafts.mdx b/docs-mintlify/guides/apps/emails/drafts.mdx index 4d02f08d9..48f7616db 100644 --- a/docs-mintlify/guides/apps/emails/drafts.mdx +++ b/docs-mintlify/guides/apps/emails/drafts.mdx @@ -14,7 +14,7 @@ Open **Emails → Drafts** in the dashboard: 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. Sending a draft (from the dashboard or from code) requires a custom email server — SMTP, Resend, or Managed — configured in the [cloud dashboard](https://app.hexclave.com). +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 diff --git a/docs-mintlify/guides/apps/emails/guide.mdx b/docs-mintlify/guides/apps/emails/guide.mdx index 512c3f0ff..35e3d617f 100644 --- a/docs-mintlify/guides/apps/emails/guide.mdx +++ b/docs-mintlify/guides/apps/emails/guide.mdx @@ -1,127 +1,198 @@ --- title: "Emails" -description: "Send custom emails, manage templates, and track delivery - all from Hexclave." +description: "Implement Hexclave emails from start to finish — server, templates, sending, and delivery." sidebarTitle: "Guide" --- -Hexclave includes a full email system for sending transactional and marketing emails to your users. It handles rendering, delivery, scheduling, notification preferences, and tracking out of the box. +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). -## Email types +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. -There are two categories of email: +## 1. Connect an email server -- **Transactional** - Required for your app to function (verification, password reset, receipts). Users cannot opt out. -- **Marketing** - Promotional or informational. Always includes an unsubscribe link. Users can opt out. +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 emails. Doing so can get your domain blacklisted by spam filters. + Never send marketing content as transactional mail. That can get your domain blacklisted. -## Sending emails +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. -Emails are sent from your server using `hexclaveServerApp.sendEmail()`. You must provide the content (HTML, a template, or a draft) and the recipients. +## 3. Brand with a theme -### Send to specific users +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 +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, and payment receipts — 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 +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 +import { hexclaveServerApp } from "@hexclave/next"; // replace `next` with your framework SDK + await hexclaveServerApp.sendEmail({ - userIds: ['user-id-1', 'user-id-2'], - subject: 'Welcome to our platform!', - html: '

Welcome!

Thanks for joining us.

', + userIds: ["user-id"], + subject: "Welcome aboard!", + html: "

Welcome!

Thanks for joining us.

", + notificationCategoryName: "Transactional", }); ``` -### Send to all users +**Template with variables:** + +```typescript +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 await hexclaveServerApp.sendEmail({ allUsers: true, - templateId: 'your-template-id', - subject: 'We just shipped a big update', - variables: { - featureName: 'Dark mode', - }, + templateId: "your-template-id", + subject: "We just shipped a big update", + variables: { featureName: "Dark mode" }, + notificationCategoryName: "Marketing", }); ``` -### Send from a dashboard draft - -If you've composed an email in the dashboard's draft editor, you can trigger it programmatically: +**Dashboard draft:** ```typescript await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - draftId: 'your-draft-id', + userIds: ["user-id"], + draftId: "your-draft-id", }); ``` -### Full options - -```typescript -await hexclaveServerApp.sendEmail({ - // Recipients - exactly one of these is required: - userIds: ['user-id-1'], // specific users - // allUsers: true, // or all users in your project - - // Content - exactly one of these is required: - html: '

Hello!

', // raw HTML - // templateId: 'template-id', // or a template with variables - // draftId: 'draft-id', // or a dashboard draft - - // Optional: - subject: 'Hello!', - variables: { key: 'value' }, - themeId: 'theme-id', // apply a specific theme - // themeId: null, // use the default theme - // themeId: false, // send with no theme at all - notificationCategoryName: 'Marketing', - scheduledAt: new Date('2027-12-25T00:00:00Z'), -}); -``` - -### `SendEmailOptions` type shape +### Options reference ```typescript type SendEmailOptions = - // Common options: & { - subject?: string; // subject line - themeId?: string | null | false; // theme override - notificationCategoryName?: string; // preference category - variables?: Record; // template variables - scheduledAt?: Date; // delay delivery until this time + subject?: string; + themeId?: string | null | false; + notificationCategoryName?: string; + variables?: Record; + scheduledAt?: Date; } - // Recipients — exactly one: & ({ userIds: string[] } | { allUsers: true }) - // Content — exactly one: & ({ html: string } | { templateId: string } | { draftId: string }); ``` - - `sendEmail` requires a custom email server (SMTP, Resend, or Managed). It cannot be used with the shared development server. - - -### Error handling - -`sendEmail` resolves to `void` and **throws** on failure. The errors it throws carry a stable string `errorCode` you can branch on - catch the cases you care about and rethrow the rest: +`sendEmail` resolves to `void` and **throws** on failure. Branch on stable `errorCode` values when present: ```typescript try { await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - html: '

Hello!

', - subject: 'Test Email', + userIds: ["user-id"], + html: "

Hello!

", + subject: "Test Email", + notificationCategoryName: "Transactional", }); } catch (error) { const errorCode = (error as { errorCode?: string }).errorCode; switch (errorCode) { - case 'REQUIRES_CUSTOM_EMAIL_SERVER': - // Configure a custom email server (SMTP, Resend, or Managed) - break; - case 'USER_ID_DOES_NOT_EXIST': + case "USER_ID_DOES_NOT_EXIST": // One or more user IDs do not exist break; - case 'SCHEMA_ERROR': + case "SCHEMA_ERROR": // Invalid email data provided break; default: @@ -130,132 +201,63 @@ try { } ``` -## Scheduling +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`. -Pass a `scheduledAt` date to delay delivery. The email enters the pipeline immediately but won't be sent until the scheduled time. +## 6. Schedule a send (optional) + +Pass `scheduledAt` to enqueue now and deliver later. Omit it to send as soon as the pipeline allows. ```typescript await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - html: '

Happy New Year!

', - subject: 'Happy New Year!', - scheduledAt: new Date('2027-01-01T00:00:00Z'), + userIds: ["user-id"], + html: "

Happy New Year!

", + subject: "Happy New Year!", + notificationCategoryName: "Marketing", + scheduledAt: new Date("2027-01-01T00:00:00Z"), }); ``` -If `scheduledAt` is omitted, the email is sent as soon as possible. +## 7. Watch delivery -## Email pipeline +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. -Emails are processed asynchronously through a multi-stage pipeline: - -1. **Enqueue** - The email is saved to the outbox with its template, recipients, and scheduling metadata. -2. **Render** - The template TSX is compiled into HTML, subject, and plain text. -3. **Queue** - Rendered emails whose scheduled time has passed are queued for delivery, respecting your project's sending capacity. -4. **Send** - Emails are delivered, honoring notification preferences and skipping users who have unsubscribed. -5. **Track** - Delivery events (sent, bounced, marked as spam) are recorded. - -You can monitor every email's status in the dashboard under **Emails → Sent**. - -## Templates & themes - -Email content is authored as **templates** (React Email TSX with typed variables) and wrapped in **themes** (shared layout and branding). Hexclave ships built-in templates for the common auth flows and three built-in themes, and you can customize or create your own from the dashboard. Reference a template when sending with `templateId`, and a theme with `themeId`. - -See [Templates & themes](./templates-and-themes) for the full reference - the template and theme APIs, built-in templates, and the built-in themes. - -## Notification preferences - -Emails are categorized as either **Transactional** or **Marketing**. Users can opt out of Marketing emails but not Transactional ones. - -When sending, specify the category: - -```typescript -await hexclaveServerApp.sendEmail({ - userIds: ['user-id'], - html: '

Check out our new feature!

', - subject: 'Product Updates', - notificationCategoryName: 'Marketing', -}); -``` - -If a user has unsubscribed from Marketing emails, the email will be automatically skipped during delivery. Marketing emails always include an unsubscribe link. - -## React components integration - -Emails integrate with Hexclave UI components automatically (for example verification, password reset, and magic-link flows). -For custom flows, trigger `sendEmail` from your server code: - -```typescript -import { hexclaveServerApp } from '@hexclave/next'; // replace `next` with the correct framework SDK package - -export async function inviteUser(userId: string) { - // sendEmail resolves to void and throws on failure - await hexclaveServerApp.sendEmail({ - userIds: [userId], - templateId: 'invitation-template', - subject: "You're invited!", - variables: { - inviteUrl: 'https://yourapp.com/invite/token123', - }, - }); -} -``` - -## Email server configuration - -Configure your email server in the dashboard under **Emails → Email Settings**. There are four options: - -### Shared (development only) - -The default for new projects. Emails are sent from `noreply@sent-with-hexclave.com` using Hexclave's shared infrastructure. Good for development - not suitable for production. - -### Custom SMTP - -Connect any SMTP provider. Configure: - -- **Host** - e.g. `smtp.sendgrid.net` -- **Port** - typically 587 (STARTTLS) or 465 (implicit TLS) -- **Username** and **Password** -- **Sender email** and **Sender name** - -### Resend - -Connect your [Resend](https://resend.com) account by entering your API key. Hexclave configures the SMTP connection automatically. - -### Managed - -Let Hexclave manage your email domain. Hexclave handles DNS configuration and deliverability for you. Set up requires: - -1. Choose a subdomain (e.g. `mail.yourapp.com`) -2. Add the DNS records Hexclave provides -3. Verify the domain in the dashboard - - - The dashboard tests your email configuration automatically when you save it by sending a test email. - - -## Delivery stats - -Hexclave tracks delivery metrics across multiple time windows (hour, day, week, month): - -- **Sent** - Successfully delivered -- **Bounced** - Rejected by the recipient's mail server -- **Marked as spam** - Recipient flagged the email - -Access these programmatically: +From code: ```typescript const info = await hexclaveServerApp.getEmailDeliveryStats(); -// info.stats.day.sent, info.stats.day.bounced, etc. -// info.capacity.rate_per_second, info.capacity.is_boost_active, etc. +// info.stats.day.sent, info.stats.day.bounced, info.stats.day.marked_as_spam +// info.capacity.rate_per_second, info.capacity.is_boost_active, ... ``` -Delivery capacity is managed automatically based on your sending reputation. If you need to temporarily increase throughput, you can activate a capacity boost: +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 await hexclaveServerApp.activateEmailCapacityBoost(); ``` -## Drafts +Or increase capacity from the dashboard: open **Emails → Sent**, find the **Domain Reputation** card, and click **Temporarily increase capacity**. -Compose emails visually in the dashboard under **Emails → Drafts**, then send from the UI or with `draftId`. See [Drafts](./drafts) for creating drafts, cloning from a template, and sending. +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 diff --git a/docs-mintlify/guides/apps/emails/overview.mdx b/docs-mintlify/guides/apps/emails/overview.mdx index 05df2fb45..d4f25d222 100644 --- a/docs-mintlify/guides/apps/emails/overview.mdx +++ b/docs-mintlify/guides/apps/emails/overview.mdx @@ -121,4 +121,4 @@ Yes. The dashboard has a full draft editor with live preview, theme selection, a 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 the details - every option, the template and theme APIs, the delivery pipeline, and configuration? Read the [Emails guide](./guide). +Ready for a start-to-finish walkthrough — server setup, templates, sending, and delivery? Read the [Emails guide](./guide). diff --git a/docs-mintlify/guides/apps/emails/templates-and-themes.mdx b/docs-mintlify/guides/apps/emails/templates-and-themes.mdx index bd816d8e4..d78404b03 100644 --- a/docs-mintlify/guides/apps/emails/templates-and-themes.mdx +++ b/docs-mintlify/guides/apps/emails/templates-and-themes.mdx @@ -107,4 +107,4 @@ Set a default theme for your project in the dashboard. You can also override the ## Related - [Drafts](./drafts) - compose in the dashboard and send with `draftId` (not TSX templates). -- [Emails guide](./guide) - sending emails, scheduling, the delivery pipeline, and server configuration. +- [Emails guide](./guide) - start-to-finish implementation: server, templates, sending, and delivery. From e05852b0caf12da6fef5265dd7683354091c4f36 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:13:58 -0500 Subject: [PATCH 24/40] emphasis on more tables you can search by. Not just events. --- docs-mintlify/guides/apps/analytics/overview.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index cc60e42cc..f129ab8b7 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -9,7 +9,7 @@ The Analytics app gives you direct access to your project's analytics dataset in Analytics is organized into four areas in the dashboard: -- **Tables**: Browse event rows with sorting, search, and incremental loading +- **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**: Visualize where users click across your pages @@ -54,7 +54,7 @@ Session replay recording is also on by default once Analytics is enabled. See [R The **Tables** screen is the fastest way to inspect recent analytics records. -- Currently shows the `events` table +- 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 From ce4113249825639ef511f61bb004a0f96862de99 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:15:01 -0500 Subject: [PATCH 25/40] updates stack refs to hexclave --- docs-mintlify/guides/apps/analytics/overview.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index f129ab8b7..7d14a657e 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -18,13 +18,13 @@ See [Replays & Clickmaps](./replays-and-clickmaps) for the full guide to session ## How Analytics Works -Stack records analytics events and replay chunks, then exposes them through the Analytics app for read-only querying and investigation. +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 Stack event ingestion, which stores data in ClickHouse. This data powers the Tables view, the SQL query runner, and the Session replay UI. +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. ### What Gets Tracked -Stack collects both client-side and server-side analytics events: +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` @@ -46,7 +46,7 @@ To use analytics in your project: 4. Open the app and navigate/click around 5. Check **Analytics -> Tables** to confirm events are arriving -After setup, Stack automatically captures client-side `$page-view` and `$click` events. +After setup, Hexclave automatically captures client-side `$page-view` and `$click` events. Session replay recording is also on by default once Analytics is enabled. See [Replays & Clickmaps](./replays-and-clickmaps) to tune privacy or opt out. From 4e45cdecf1fe5cee537a06cdc99769c670dd1a4a Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:16:24 -0500 Subject: [PATCH 26/40] encourage cross-adoption --- docs-mintlify/guides/apps/analytics/overview.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index 7d14a657e..b89798ddb 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -22,6 +22,8 @@ Hexclave records analytics events and replay chunks, then exposes them through t 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: From 496b88eec6d04b5e804a42467bb6e4078884cc64 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:19:59 -0500 Subject: [PATCH 27/40] more clear wording around opt out for analytics --- docs-mintlify/guides/apps/analytics/overview.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index b89798ddb..9f854dc27 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -43,14 +43,16 @@ To use analytics in your project: ## Quick Start 1. Enable Analytics in your Hexclave dashboard (**Apps -> Analytics**) -2. Initialize Hexclave on your frontend with `HexclaveClientApp`/`HexclaveProvider` +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. -Session replay recording is also on by default once Analytics is enabled. See [Replays & Clickmaps](./replays-and-clickmaps) to tune privacy or opt out. +See [Replays & Clickmaps](./replays-and-clickmaps) to tune replay privacy or opt out. ## Tables From 38897fbc54c26a4ebfd7d8cb420ef45d8ad826e7 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:26:19 -0500 Subject: [PATCH 28/40] more clear wording around clickmaps --- docs-mintlify/guides/apps/analytics/overview.mdx | 9 +++++---- .../guides/apps/analytics/replays-and-clickmaps.mdx | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index 9f854dc27..9d8f5005b 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -12,7 +12,7 @@ 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**: Visualize where users click across your pages +- **Clickmaps**: See which elements get clicks on your pages (element-tied, not coordinate heatmaps) See [Replays & Clickmaps](./replays-and-clickmaps) for the full guide to session replays and clickmaps. @@ -99,7 +99,7 @@ export const hexclaveClientApp = new HexclaveClientApp({ tokenStore: "cookie", // use "nextjs-cookie" in Next.js analytics: { replays: { - // Enabled by default; set to false to opt out. + // Optional. Defaults to true when the Analytics app is enabled; set to false to opt out. enabled: true, // Optional. Defaults to true. maskAllInputs: true, @@ -128,7 +128,8 @@ This stops the SDK from sending `$page-view` and `$click` events. If you'd rathe ## Best Practices -1. **Use Tables for quick incident triage**: the Tables UI is the fastest way to inspect recent `events` rows without writing SQL. +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. **Keep replay privacy defaults on**: leave `maskAllInputs` enabled unless you have a specific reason and a data-handling policy for unmasked inputs. +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. diff --git a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx index 29e8d9ab0..19143a9da 100644 --- a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx +++ b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx @@ -96,7 +96,7 @@ Recorded replay data is stored privately (the DOM recording is gzipped and kept ## 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. Click data comes from the same `$click` events the SDK already captures, so there's nothing extra to instrument. +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 From 321c12ba3cbf6d224838d6fedca467407b40f7e5 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:28:35 -0500 Subject: [PATCH 29/40] remove heatmaps wording --- docs-mintlify/guides/apps/analytics/overview.mdx | 2 +- docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-mintlify/guides/apps/analytics/overview.mdx b/docs-mintlify/guides/apps/analytics/overview.mdx index 9d8f5005b..b6396e4a9 100644 --- a/docs-mintlify/guides/apps/analytics/overview.mdx +++ b/docs-mintlify/guides/apps/analytics/overview.mdx @@ -12,7 +12,7 @@ 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 (element-tied, not coordinate heatmaps) +- **Clickmaps**: See which elements get clicks on your pages See [Replays & Clickmaps](./replays-and-clickmaps) for the full guide to session replays and clickmaps. diff --git a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx index 19143a9da..072f87bb8 100644 --- a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx +++ b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx @@ -6,7 +6,7 @@ description: "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. - Hexclave's spatial click visualization is called a **Clickmap** - it aggregates clicks per on-page element (with dead-click detection), rather than rendering a coordinate-density "heatmap." If you're looking for "heatmaps," this is the feature you want. + 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 From 3e9031571216ba166f1902443d6565d1885b4001 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:30:29 -0500 Subject: [PATCH 30/40] Remove mentions of rrweb deps --- docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx index 072f87bb8..97a952f1c 100644 --- a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx +++ b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx @@ -21,7 +21,7 @@ When those hold, the SDK captures `$page-view` and `$click` events and (for repl ## Session Replays -Session replays let you move from "an event happened" to "what the user actually saw." Recording is powered by [rrweb](https://www.rrweb.io/): the SDK captures DOM snapshots and mutations, mouse interactions, and page state over time, then plays them back as a faithful reconstruction of the session. +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 From e16dd0a73c883816b0412f3570e25601305bea8f Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:34:12 -0500 Subject: [PATCH 31/40] change wording around user sessions for replays and clickmaps --- docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx index 97a952f1c..69c467df6 100644 --- a/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx +++ b/docs-mintlify/guides/apps/analytics/replays-and-clickmaps.mdx @@ -14,8 +14,8 @@ Beyond raw events and SQL, Analytics ships two visual debugging tools: **Session 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). -3. The visitor has an **authenticated session**. Capture is tied to the user's session, so anonymous traffic isn't recorded. +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. From af605b7d288c8fd823af799555db491dd0d55eb0 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:37:32 -0500 Subject: [PATCH 32/40] remove mentions of turnstile scores --- .../guides/apps/authentication/fraud-protection.mdx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx index 835a33f8d..c460a3e6d 100644 --- a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx +++ b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx @@ -4,7 +4,7 @@ description: "Detect bots, free-trial abuse, and other fraudulent sign-ups." sidebarTitle: "Fraud Protection" --- -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, geo-IP country, and Cloudflare Turnstile result), and the conditions you can write against them in [Sign-up Rules](./sign-up-rules). +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 geo-IP 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. @@ -19,10 +19,8 @@ Every sign-up attempt is scored with the following fields. They're always availa | Field | Type | Operators | Description | |---|---|---|---| | `countryCode` | string (ISO-3166-1 alpha-2) | `equals`, `not_equals`, `in_list` | Geo-IP country of the request (e.g. `"US"`, `"DE"`, `"NG"`). Empty if it can't be resolved. | -| `riskScores.bot` | number (0-100) | `equals`, `not_equals`, `greater_than`, `greater_or_equal`, `less_than`, `less_or_equal` | Confidence that the sign-up is automated. Higher = more likely a bot. Hexclave uses signals like the Cloudflare Turnstile verdict to compute this score. | -| `riskScores.free_trial_abuse` | number (0-100) | same as `riskScores.bot` | Confidence that the user is attempting to abuse a free-trial / new-account incentive (multi-accounting, disposable infra, etc.). | - -The rule tester additionally exposes a **Turnstile** override (`ok` / `invalid` / `error`) so you can simulate how a given Turnstile verdict affects the resulting bot score. +| `riskScores.bot` | number (0-100) | `equals`, `not_equals`, `greater_than`, `greater_or_equal`, `less_than`, `less_or_equal` | Confidence that the sign-up is automated. Higher = more likely a bot. | +| `riskScores.free_trial_abuse` | number (0-100) | same as `riskScores.bot` | Confidence that the user is attempting to abuse a free-trial / new-account incentive. | These fields evaluate together with the rest of the sign-up context, so a single rule can combine them freely. @@ -64,7 +62,6 @@ The Sign-up Rules **rule tester** (button **Open tester** at the bottom of the p - **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. -- **Turnstile** - `Default (real result)` / `OK` / `Invalid` / `Error`. The tester doesn't run a live Turnstile check; when left at the default, the simulated verdict falls back to `Invalid` for score derivation, so set an explicit value to model a passing challenge. Click **Run test** to see how each rule evaluates against the simulated context. The result panel shows: From 76387514bd610b9ac528286c16a84468ac378929 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:38:56 -0500 Subject: [PATCH 33/40] update note about auth app always enabled --- docs-mintlify/guides/apps/authentication/fraud-protection.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx index c460a3e6d..d0f9b559f 100644 --- a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx +++ b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx @@ -9,7 +9,7 @@ Fraud Protection is a sub-app of [Authentication](./overview). It isn't a separa 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. - Fraud Protection inherits its enabled state from its parent app, Authentication. If Authentication is on, Fraud Protection is on. There is no independent toggle. + Since Authentication is always on, Fraud Protection is on. There is no independent toggle. ## Available signals From b48a487a340bd3e0774d9fa812647109a4bae920 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:45:08 -0500 Subject: [PATCH 34/40] update restricted user wording, and clarifications --- .../guides/apps/authentication/fraud-protection.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx index d0f9b559f..d31266287 100644 --- a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx +++ b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx @@ -38,7 +38,9 @@ Open **Authentication → Sign-up Rules → Add rule**. The rule builder is the Send borderline accounts to a restricted state instead of blocking outright, so support can review them: - Condition: `riskScores.free_trial_abuse >= 60` -- Action: **Restrict** - see [Sign-up Rules → Restrict](./sign-up-rules#restrict) for how restricted users appear in JWTs and the dashboard. +- 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 @@ -120,4 +122,5 @@ The banner also exposes the same restriction action button so you can manage it - [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. From 81679a113663b41c49e213523d5b48e984e8cd56 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:48:25 -0500 Subject: [PATCH 35/40] shared keys wording update --- docs-mintlify/guides/apps/authentication/auth-providers.mdx | 2 ++ docs-mintlify/guides/apps/authentication/overview.mdx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/authentication/auth-providers.mdx b/docs-mintlify/guides/apps/authentication/auth-providers.mdx index f68600c03..4edfc871d 100644 --- a/docs-mintlify/guides/apps/authentication/auth-providers.mdx +++ b/docs-mintlify/guides/apps/authentication/auth-providers.mdx @@ -39,6 +39,8 @@ Authentication providers determine how users can sign in to your application. He 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. diff --git a/docs-mintlify/guides/apps/authentication/overview.mdx b/docs-mintlify/guides/apps/authentication/overview.mdx index 79ccdba54..33815c81a 100644 --- a/docs-mintlify/guides/apps/authentication/overview.mdx +++ b/docs-mintlify/guides/apps/authentication/overview.mdx @@ -42,7 +42,7 @@ Yes - and you flip each one on or off from the dashboard, no redeploy required. -Use Hexclave's shared development keys for the major providers (Google, GitHub, Microsoft, Spotify) while you build, then swap in your own credentials for production. See [all auth providers](./auth-providers) for setup details. +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? From af9241d0f8bb5d11aa7ce4f9444167229913e5de Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:51:12 -0500 Subject: [PATCH 36/40] update wording around SDK/APIs working on the same cloud v local. better wording here now --- docs-mintlify/guides/apps/authentication/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-mintlify/guides/apps/authentication/overview.mdx b/docs-mintlify/guides/apps/authentication/overview.mdx index 33815c81a..e54677ea1 100644 --- a/docs-mintlify/guides/apps/authentication/overview.mdx +++ b/docs-mintlify/guides/apps/authentication/overview.mdx @@ -80,7 +80,7 @@ Yes. Write [sign-up rules](./sign-up-rules) to allow, reject, or restrict accoun ## 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 whether you use the managed service or your own deployment. +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 From eb6084d56146aad5056bd4da76a33ca4815e09d7 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:54:55 -0500 Subject: [PATCH 37/40] Update hosted components docs, and add new documentation regarding this --- docs-mintlify/docs.json | 1 + .../guides/apps/authentication/overview.mdx | 8 +- .../going-further/hosted-vs-handler.mdx | 82 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 docs-mintlify/guides/going-further/hosted-vs-handler.mdx diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 949abbe36..2c815131c 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -77,6 +77,7 @@ "pages": [ "guides/going-further/cli", "guides/going-further/local-vs-cloud-dashboard", + "guides/going-further/hosted-vs-handler", "guides/going-further/hexclave-config" ] }, diff --git a/docs-mintlify/guides/apps/authentication/overview.mdx b/docs-mintlify/guides/apps/authentication/overview.mdx index e54677ea1..7c1a587ef 100644 --- a/docs-mintlify/guides/apps/authentication/overview.mdx +++ b/docs-mintlify/guides/apps/authentication/overview.mdx @@ -6,7 +6,7 @@ sidebarTitle: "Overview" - **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), [Auth providers](./auth-providers), [JWTs & session verification](./jwts), [Sign-up rules](./sign-up-rules), and [Connected accounts](./connected-accounts). + **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), [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). @@ -16,7 +16,7 @@ Authentication is the foundation every other Hexclave app builds on. You get hos ## Can I add sign-in without building forms? -Yes. Mount one catch-all route and you get a fully styled, accessible, maintained auth experience: sign-in, sign-up, password reset, OAuth callbacks, email verification, and account settings - all of it. +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" import { HexclaveHandler } from "@hexclave/next"; // replace `next` with your framework SDK @@ -84,8 +84,8 @@ Yes. Hexclave is open source (MIT client, AGPLv3 server). Run it fully self-host ## Start here -1. [Set up Hexclave](/guides/getting-started/setup) in your project (a few minutes). -2. Mount `` at `/handler/[...]` and turn on the auth methods you want. +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. diff --git a/docs-mintlify/guides/going-further/hosted-vs-handler.mdx b/docs-mintlify/guides/going-further/hosted-vs-handler.mdx new file mode 100644 index 000000000..3919d8a6c --- /dev/null +++ b/docs-mintlify/guides/going-further/hosted-vs-handler.mdx @@ -0,0 +1,82 @@ +--- +title: "Hosted Components vs. Handler" +description: "Choose where Hexclave auth pages live — hosted by Hexclave, or on your own domain with HexclaveHandler." +sidebarTitle: "Hosted vs. Handler" +--- + +Hexclave can render sign-in, sign-up, password reset, and the other auth pages in two ways. You pick the mode with the SDK `urls` option. + +| Mode | Config | Where pages live | Best for | +| --- | --- | --- | --- | +| **Hosted components** (recommended) | `urls: { default: { type: "hosted" } }` | Hexclave-hosted URLs for your project | New projects, least maintenance | +| **Own handler** | `urls: { default: { type: "handler-component" } }` plus `` | Routes on your domain (typically `/handler/...`) | Same-domain auth UI, frameworks that need a local catch-all | + +You can also point individual keys (`signIn`, `accountSettings`, …) at a custom path or mix hosted and handler targets. See [Setup](/guides/getting-started/setup) for framework-specific wiring. + +## Prefer hosted components + +For new projects, set: + +```ts +export const hexclaveClientApp = new HexclaveClientApp({ + tokenStore: "cookie", // or "nextjs-cookie" on Next.js + urls: { + default: { + type: "hosted", + }, + }, +}); +``` + +With hosted components: + +- Users land on Hexclave-hosted auth pages that stay up to date automatically. +- You do **not** need a `/handler/[...]` catch-all in your app for the default sign-in flow. +- Redirect helpers such as `redirectToSignIn()` send people to those hosted pages, then back to your app. + +This is the path Setup and the CLI onboarding flow recommend. + +## Own handler on your domain + +Use a local handler when you want auth UI on your own origin (same cookies / branding constraints, or an older integration). + +1. Point `urls.default` at the handler component (this is also the SDK default if you omit `urls.default`): + +```ts +urls: { + default: { + type: "handler-component", + }, +}, +``` + +2. Mount the catch-all route your framework SDK documents — for Next.js: + +```tsx title="app/handler/[...hexclave]/page.tsx" +import { HexclaveHandler } from "@hexclave/next"; + +export default function Handler() { + return ; +} +``` + +Auth URLs then look like `/handler/sign-in`, `/handler/sign-up`, and so on on **your** domain. The handler component is only available in some framework SDKs; hosted works everywhere the client SDK can redirect. + + + Older docs and reminders may say `type: "handler"`. The current target is `{ type: "handler-component" }`. Prefer `type: "hosted"` for new work. + + +## Mixing and custom pages + +You do not have to pick one mode for every page. Examples: + +- Keep `default: { type: "hosted" }`, but set `accountSettings: "/settings"` (or `{ type: "custom", url: "/settings", version: 0 }`) for a page you own. +- Keep most pages on the handler, but send `signIn: { type: "hosted" }` if you want only sign-in hosted. + +Whenever you add a custom auth page, update the matching `urls` key and any post-auth redirects (`afterSignIn`, `afterSignUp`, `afterSignOut`, `home`). Those keys are the source of truth for `redirectToSignIn()` and related helpers — if they still point at defaults after you customize routes, users can hit extra redirects or land on the wrong page. + +## Related + +- [Setup](/guides/getting-started/setup) — framework setup, including the hosted `urls` default. +- [Authentication overview](/guides/apps/authentication/overview) — what the Authentication app covers. +- [Local vs. Cloud Dashboard](/guides/going-further/local-vs-cloud-dashboard) — development environment vs cloud project. From 9162767c5cd707493ba5b8d0a70b366a4a874c58 Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 04:58:45 -0500 Subject: [PATCH 38/40] remove details from fraud bot implementation details --- .../guides/apps/authentication/fraud-protection.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx index d31266287..9bf8bb791 100644 --- a/docs-mintlify/guides/apps/authentication/fraud-protection.mdx +++ b/docs-mintlify/guides/apps/authentication/fraud-protection.mdx @@ -4,7 +4,7 @@ description: "Detect bots, free-trial abuse, and other fraudulent sign-ups." sidebarTitle: "Fraud Protection" --- -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 geo-IP country), and the conditions you can write against them in [Sign-up Rules](./sign-up-rules). +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. @@ -18,9 +18,9 @@ Every sign-up attempt is scored with the following fields. They're always availa | Field | Type | Operators | Description | |---|---|---|---| -| `countryCode` | string (ISO-3166-1 alpha-2) | `equals`, `not_equals`, `in_list` | Geo-IP country of the request (e.g. `"US"`, `"DE"`, `"NG"`). Empty if it can't be resolved. | -| `riskScores.bot` | number (0-100) | `equals`, `not_equals`, `greater_than`, `greater_or_equal`, `less_than`, `less_or_equal` | Confidence that the sign-up is automated. Higher = more likely a bot. | -| `riskScores.free_trial_abuse` | number (0-100) | same as `riskScores.bot` | Confidence that the user is attempting to abuse a free-trial / new-account incentive. | +| `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. From b1d9f6db40dded5e4c205a1f2cc292d4a2b7226c Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 05:03:59 -0500 Subject: [PATCH 39/40] Rename stack -> hexclave in docs, and one place in data-vault example on dashboard. --- .../data-vault/stores/[storeId]/page-client.tsx | 6 +++--- .../guides/apps/authentication/user-onboarding.mdx | 4 ++-- .../guides/apps/launch-checklist/overview.mdx | 2 +- docs-mintlify/guides/apps/payments/checkout.mdx | 2 +- .../guides/apps/payments/granting-products.mdx | 4 ++++ .../guides/apps/payments/items-and-entitlements.mdx | 2 +- docs-mintlify/guides/apps/payments/subscriptions.mdx | 2 ++ .../other/tutorials/build-a-team-based-app.mdx | 12 ++++++------ .../other/tutorials/ship-production-ready-auth.mdx | 6 +++--- 9 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx index 00f88aabb..f1161103a 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/stores/[storeId]/page-client.tsx @@ -137,7 +137,7 @@ export default function PageClient({ storeId }: PageClientProps) { const serverExample = deindent` // In your .env file or environment variables: - // STACK_DATA_VAULT_SECRET=insert-a-randomly-generated-secret-here + // HEXCLAVE_DATA_VAULT_SECRET=insert-a-randomly-generated-secret-here const store = await hexclaveServerApp.getDataVaultStore(${JSON.stringify(storeId)}); @@ -146,12 +146,12 @@ export default function PageClient({ storeId }: PageClientProps) { // Get a value for a specific key const value = await store.getValue(key, { - secret: process.env.STACK_DATA_VAULT_SECRET, + secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); // Set a value for a specific key await store.setValue(key, "my-value", { - secret: process.env.STACK_DATA_VAULT_SECRET, + secret: process.env.HEXCLAVE_DATA_VAULT_SECRET, }); `; diff --git a/docs-mintlify/guides/apps/authentication/user-onboarding.mdx b/docs-mintlify/guides/apps/authentication/user-onboarding.mdx index 312f50d58..9ecb88f84 100644 --- a/docs-mintlify/guides/apps/authentication/user-onboarding.mdx +++ b/docs-mintlify/guides/apps/authentication/user-onboarding.mdx @@ -74,7 +74,7 @@ Next, we can create a hook/function to check if the user has completed onboardin ```jsx - import { hexclaveServerApp } from '@/stack/server'; + import { hexclaveServerApp } from '@/hexclave/server'; import { redirect } from 'next/navigation'; export async function ensureOnboarded() { @@ -108,7 +108,7 @@ You can then use these functions wherever onboarding is required: ```jsx import { ensureOnboarding } from '@/app/onboarding-functions'; - import { hexclaveServerApp } from '@/stack/server'; + import { hexclaveServerApp } from '@/hexclave/server'; export default async function HomePage() { await ensureOnboarding(); diff --git a/docs-mintlify/guides/apps/launch-checklist/overview.mdx b/docs-mintlify/guides/apps/launch-checklist/overview.mdx index c7e19033c..1724c5fc1 100644 --- a/docs-mintlify/guides/apps/launch-checklist/overview.mdx +++ b/docs-mintlify/guides/apps/launch-checklist/overview.mdx @@ -14,7 +14,7 @@ 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/stack-app)). + 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. diff --git a/docs-mintlify/guides/apps/payments/checkout.mdx b/docs-mintlify/guides/apps/payments/checkout.mdx index cd3b74c8e..60c21b886 100644 --- a/docs-mintlify/guides/apps/payments/checkout.mdx +++ b/docs-mintlify/guides/apps/payments/checkout.mdx @@ -32,7 +32,7 @@ The `createCheckoutUrl` method is available on both user and team objects. ```typescript title="app/purchase/page.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function PurchasePage() { const user = await hexclaveServerApp.getUser({ or: 'redirect' }); diff --git a/docs-mintlify/guides/apps/payments/granting-products.mdx b/docs-mintlify/guides/apps/payments/granting-products.mdx index 1a267b165..76596106d 100644 --- a/docs-mintlify/guides/apps/payments/granting-products.mdx +++ b/docs-mintlify/guides/apps/payments/granting-products.mdx @@ -8,6 +8,8 @@ Sometimes you need to give a customer a product without charging them - free tri ## Granting a configured product ```typescript +import { hexclaveServerApp } from "@/hexclave/server"; + // Grant a pre-configured product to a user await hexclaveServerApp.grantProduct({ userId: "user-id", @@ -34,6 +36,8 @@ If you already have a reference to a **server** user or team object (for example 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 +import { hexclaveServerApp } from "@/hexclave/server"; + await hexclaveServerApp.grantProduct({ userId: "user-id", product: { diff --git a/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx b/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx index 204d83f12..d5e9533ab 100644 --- a/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx +++ b/docs-mintlify/guides/apps/payments/items-and-entitlements.mdx @@ -50,7 +50,7 @@ export default function CreditsWidget() { 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" -import { hexclaveServerApp } from "@/stack/server"; +import { hexclaveServerApp } from "@/hexclave/server"; export async function consumeCredits(userId: string, amount: number) { const user = await hexclaveServerApp.getUser(userId); diff --git a/docs-mintlify/guides/apps/payments/subscriptions.mdx b/docs-mintlify/guides/apps/payments/subscriptions.mdx index acbe57a3c..bcefd4fbc 100644 --- a/docs-mintlify/guides/apps/payments/subscriptions.mdx +++ b/docs-mintlify/guides/apps/payments/subscriptions.mdx @@ -57,6 +57,8 @@ export default function UpgradeButton() { ```typescript + import { hexclaveServerApp } from "@/hexclave/server"; + // Cancel for the current user await hexclaveServerApp.cancelSubscription({ productId: "prod_pro" }); diff --git a/docs-mintlify/guides/other/tutorials/build-a-team-based-app.mdx b/docs-mintlify/guides/other/tutorials/build-a-team-based-app.mdx index db57f452c..9cb21e21d 100644 --- a/docs-mintlify/guides/other/tutorials/build-a-team-based-app.mdx +++ b/docs-mintlify/guides/other/tutorials/build-a-team-based-app.mdx @@ -17,7 +17,7 @@ If you have not installed Stack yet (handler routes, `HexclaveProvider`, environ ## Prerequisites -- Hexclave installed as in [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave) (Next.js App Router examples below assume `hexclaveServerApp` in `stack/server.ts` and `@hexclave/next` in the app). +- Hexclave installed as in [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave) (Next.js App Router examples below assume `hexclaveServerApp` in `hexclave/server.ts` and `@hexclave/next` in the app). - A project in the [dashboard](https://app.hexclave.com/projects) where you can edit **Teams** and **Team permissions**. @@ -42,7 +42,7 @@ Use the **current user** as the scope: `listTeams` / `useTeams`, and `getTeam` / ```tsx title="app/team/[teamId]/page.tsx" import Link from "next/link"; - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; type PageProps = { params: { teamId: string } }; @@ -101,7 +101,7 @@ If your framework types route `params` as a `Promise` (newer Next.js), `await` t ```tsx title="app/team/page.tsx" import Link from "next/link"; - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function TeamsIndexPage() { const user = await hexclaveServerApp.getUser({ or: "redirect" }); @@ -179,7 +179,7 @@ export function CreateWorkspaceButton() { For imports, support tools, or other **server** jobs, `hexclaveServerApp.createTeam` creates a team at project scope (see [Teams](/guides/apps/teams/overview)); wire membership separately if your flow requires it. ```tsx title="scripts/provision-team.example.ts" -import { hexclaveServerApp } from "@/stack/server"; +import { hexclaveServerApp } from "@/hexclave/server"; export async function provisionEmptyTeam(displayName: string) { return await hexclaveServerApp.createTeam({ displayName }); @@ -226,7 +226,7 @@ Replace `export_reports` with a permission you created. ```tsx title="app/team/[teamId]/reports/page.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; type PageProps = { params: { teamId: string } }; @@ -289,7 +289,7 @@ For listing effective permissions, use `listPermissions` / `usePermissions` ([RB ```tsx title="app/actions/reports.ts" "use server"; -import { hexclaveServerApp } from "@/stack/server"; +import { hexclaveServerApp } from "@/hexclave/server"; export async function requestReportExportAction(teamId: string) { const user = await hexclaveServerApp.getUser({ or: "throw" }); diff --git a/docs-mintlify/guides/other/tutorials/ship-production-ready-auth.mdx b/docs-mintlify/guides/other/tutorials/ship-production-ready-auth.mdx index 990e40ab9..012e1426e 100644 --- a/docs-mintlify/guides/other/tutorials/ship-production-ready-auth.mdx +++ b/docs-mintlify/guides/other/tutorials/ship-production-ready-auth.mdx @@ -27,7 +27,7 @@ You typically combine **one or more** of: ```tsx title="middleware.ts" import { NextRequest, NextResponse } from "next/server"; - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export async function middleware(request: NextRequest) { const user = await hexclaveServerApp.getUser(); @@ -48,7 +48,7 @@ You typically combine **one or more** of: ```tsx title="app/app/dashboard/page.tsx" - import { hexclaveServerApp } from "@/stack/server"; + import { hexclaveServerApp } from "@/hexclave/server"; export default async function DashboardPage() { await hexclaveServerApp.getUser({ or: "redirect" }); @@ -76,7 +76,7 @@ You typically combine **one or more** of: - **`{ or: "throw" }`** — use in **server actions**, **route handlers**, and other places where redirecting would be wrong; map errors to `401`/`403` responses yourself. ```tsx title="app/api/me/route.ts" -import { hexclaveServerApp } from "@/stack/server"; +import { hexclaveServerApp } from "@/hexclave/server"; import { NextResponse } from "next/server"; export async function GET() { From 19f728271c30904b9c5210e20355671c54976a2b Mon Sep 17 00:00:00 2001 From: Madison Date: Fri, 17 Jul 2026 13:31:34 -0500 Subject: [PATCH 40/40] update generated docs json --- .../docs-json.generated.ts | 113 +++++++++++++++++- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts b/packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts index 0b3eb3dfd..5c0b51df0 100644 --- a/packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts +++ b/packages/shared/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts @@ -79,6 +79,7 @@ const docsJson = { "pages": [ "guides/going-further/cli", "guides/going-further/local-vs-cloud-dashboard", + "guides/going-further/hosted-vs-handler", "guides/going-further/hexclave-config" ] }, @@ -95,6 +96,7 @@ const docsJson = { "guides/apps/authentication/connected-accounts", "guides/apps/authentication/jwts", "guides/apps/authentication/sign-up-rules", + "guides/apps/authentication/fraud-protection", "guides/apps/authentication/cli-authentication", { "group": "All Auth Providers", @@ -109,18 +111,50 @@ const docsJson = { "guides/apps/authentication/auth-providers/google", "guides/apps/authentication/auth-providers/linkedin", "guides/apps/authentication/auth-providers/microsoft", - "guides/apps/authentication/auth-providers/passkey", "guides/apps/authentication/auth-providers/spotify", "guides/apps/authentication/auth-providers/twitch", + "guides/apps/authentication/auth-providers/x-twitter", + "guides/apps/authentication/auth-providers/passkey", "guides/apps/authentication/auth-providers/two-factor-auth", - "guides/apps/authentication/auth-providers/x-twitter" + "guides/apps/authentication/auth-providers/custom-oidc" ] } ] }, - "guides/apps/emails/overview", - "guides/apps/payments/overview", - "guides/apps/analytics/overview", + { + "group": "Emails", + "icon": "/images/app-icons/emails.svg", + "pages": [ + "guides/apps/emails/overview", + "guides/apps/emails/guide", + "guides/apps/emails/templates-and-themes", + "guides/apps/emails/drafts" + ] + }, + { + "group": "Payments", + "icon": "/images/app-icons/payments.svg", + "pages": [ + "guides/apps/payments/overview", + "guides/apps/payments/setup", + "guides/apps/payments/products-and-pricing", + "guides/apps/payments/items-and-entitlements", + "guides/apps/payments/checkout", + "guides/apps/payments/subscriptions", + "guides/apps/payments/billing-and-invoices", + "guides/apps/payments/granting-products", + "guides/apps/payments/customers" + ] + }, + { + "group": "Analytics", + "icon": "/images/app-icons/analytics.svg", + "pages": [ + "guides/apps/analytics/overview", + "guides/apps/analytics/queries-and-tables", + "guides/apps/analytics/replays-and-clickmaps" + ] + }, { "group": "Teams", "icon": "/images/app-icons/teams.svg", @@ -129,7 +163,6 @@ const docsJson = { "guides/apps/teams/team-selection" ] }, - "guides/apps/fraud-protection/overview", "guides/apps/rbac/overview", "guides/apps/api-keys/overview", "guides/apps/data-vault/overview", @@ -254,6 +287,74 @@ const docsJson = { ] }, "redirects": [ + { + "source": "/sdk/objects/stack-app", + "destination": "/sdk/objects/hexclave-app" + }, + { + "source": "/sdk/hooks/use-stack-app", + "destination": "/sdk/hooks/use-hexclave-app" + }, + { + "source": "/rest-api/overview", + "destination": "/api/overview" + }, + { + "source": "/getting-started/setup", + "destination": "/guides/getting-started/setup" + }, + { + "source": "/docs/getting-started/setup", + "destination": "/guides/getting-started/setup" + }, + { + "source": "/docs/next/getting-started/setup", + "destination": "/guides/getting-started/setup" + }, + { + "source": "/docs/sdk", + "destination": "/sdk/overview" + }, + { + "source": "/docs/apps/analytics", + "destination": "/guides/apps/analytics/overview" + }, + { + "source": "/docs/apps/api-keys", + "destination": "/guides/apps/api-keys/overview" + }, + { + "source": "/docs/others/convex", + "destination": "/guides/integrations/convex/overview" + }, + { + "source": "/docs/concepts/teams", + "destination": "/guides/apps/teams/overview" + }, + { + "source": "/docs/concepts/custom-user-data", + "destination": "/guides/getting-started/user-fundamentals#custom-metadata" + }, + { + "source": "/guides/going-further/user-metadata", + "destination": "/guides/getting-started/user-fundamentals#custom-metadata" + }, + { + "source": "/others/js-client", + "destination": "/sdk/objects/hexclave-app" + }, + { + "source": "/guides/going-further/stack-app", + "destination": "/sdk/objects/hexclave-app" + }, + { + "source": "/guides/apps/fraud-protection/overview", + "destination": "/guides/apps/authentication/fraud-protection" + }, + { + "source": "/guides/apps/payments/guide", + "destination": "/guides/apps/payments/setup" + }, { "source": "/guides/going-further/backend-integration", "destination": "/guides/going-further/local-vs-cloud-dashboard"