stack/docs-mintlify
BilalG1 49e3c48d64 feat(oauth): per-provider customCallbackUrl for redirect_uri (#1512)
## Summary

Replaces the request-host-header-derived OAuth `redirect_uri` with a
config-driven `customCallbackUrl` field on each environment-level OAuth
provider.

Resolution of the `redirect_uri` we send to providers (and that
customers register in their provider app config):

- **Shared providers** → always the stack-auth-branded callback, so
Stack's shared OAuth apps keep working. `customCallbackUrl` is
schema-forbidden when `isShared` is true.
- **Custom + `customCallbackUrl` set** → the configured URL verbatim.
- **Custom without it (legacy)** → the stack-auth-branded callback, so
providers registered before this field are unaffected.
- **New custom providers set up in the dashboard** → the env-aware
hexclave-branded callback (prod → `api.hexclave.com`, dev/staging →
siblings, self-host/localhost → `NEXT_PUBLIC_STACK_API_URL` unchanged).

## Details

- **Schema** (`schema.ts`, `schema-fields.ts`): optional
`customCallbackUrl` after `clientSecret`, with a `.when('isShared')`
rule rejecting any value for shared providers; added to the provider
default factory.
- **Shared host helper** (`utils/cloud-hosts.tsx`, new):
`CLOUD_HOST_PAIRS` moved into stack-shared with `getCloudApiUrlSiblings`
/ `getStackAuthApiBaseUrl` / `getHexclaveApiBaseUrl`;
`request-api-url.ts` re-exports it so the JWT `iss` logic is untouched.
- **Runtime** (`oauth/index.tsx` + all 13 provider `create()`s):
`getProvider` resolves the full `redirect_uri` from config instead of
the request host; providers now take `redirectUri` instead of `apiUrl`.
The JWT `iss` path still uses the request host.
- **Dashboard** (`page-client.tsx`, `providers.tsx`,
`oauth-callback-url.ts` new): brand-new custom providers get the
hexclave callback; existing providers keep whatever they had (edits
never silently move a registered redirect URL); the displayed Redirect
URL mirrors backend resolution.
- **Docs** (`migration.mdx`): existing `api.stack-auth.com` callbacks
keep working; only recreated providers use the hexclave URL.

## Notes / scope decisions

- **Dashboard-only injection**: SDK/CLI/legacy-config-created custom
providers fall back to the stack-auth callback (they don't auto-get the
hexclave URL).
- **shared → standard** conversions keep the stack-auth fallback rather
than flipping to hexclave (the safe path that never breaks a registered
redirect).

## Test plan

- [x] `typecheck` + `lint` green across stack-shared, backend,
dashboard, e2e
- [x] cloud-hosts unit tests, schema tests, schema fuzzer pass
- [x] e2e: shared-provider `customCallbackUrl` rejected (400);
standard-provider `customCallbackUrl` accepted and round-trips
- [ ] e2e OAuth authorize/callback flow (needs running stack) — reasoned
unaffected since localhost isn't a cloud host, so the redirect base
stays localhost as before

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a per-provider `customCallbackUrl` for OAuth `redirect_uri`,
removing the request-host dependency and making redirects predictable.
Shared providers always use the Stack-branded callback; new or converted
custom providers default to the Hexclave-branded callback. Existing
callbacks keep working; no changes needed unless you recreate or convert
a provider.

- **New Features**
- Added `customCallbackUrl` on provider configs (URL-validated;
forbidden when `isShared` is true).
- `getProvider` now resolves a config-driven `redirectUri`; providers
take `redirectUri` instead of `apiUrl` (pure resolver with in-source +
e2e tests to lock legacy behavior).
- Introduced `@stackframe/stack-shared` `utils/cloud-hosts.tsx` and
dashboard helpers to show the resolved Redirect URL and set the Hexclave
callback for new providers and when converting shared → standard.

- **Bug Fixes**
- OAuth callback now handles legitimate cross-host flows by recording
the authorize host and skipping the host-scoped CSRF cookie when
authorize and callback hosts differ, relying on server-side state and
PKCE.

<sup>Written for commit 32d95fcdcb.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/stack-auth/pull/1512?utm_source=github">Review
in cubic</a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Preserve and display custom OAuth callback/redirect URLs in the
dashboard; provider creation/edit flows respect existing custom URLs.
* Added cloud-host mapping and redirect-uri helpers to resolve branded
API callback bases.

* **Bug Fixes**
* Improved cross-host OAuth callback handling and CSRF validation for
reliable cross-host flows.

* **Tests**
* Added E2E and unit tests covering callback URL behavior and host
mapping.

* **Documentation**
* Updated migration guidance for callback URL changes and recreation
scenarios.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1512?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-02 15:22:13 -05:00
..
api feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
guides feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
images feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
openapi feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
sdk feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
snippets feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
.gitignore New setup (#1413) 2026-05-06 12:03:06 -07:00
apps-sidebar-filter.js Update docs apps filter field based on theme 2026-05-07 11:08:39 -07:00
code-language-labels.js Update User Fundamentals 2026-05-22 16:28:43 -07:00
docs.json feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
index.mdx feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481) 2026-06-02 15:22:13 -05:00
migration.mdx feat(oauth): per-provider customCallbackUrl for redirect_uri (#1512) 2026-06-02 15:22:13 -05:00
package.json chore: update package versions 2026-06-02 15:20:58 -05:00
README.md Rename port prefix envvar 2026-06-02 15:22:13 -05:00
style.css Fix docs sidebar icons 2026-06-02 15:20:57 -05:00

docs-mintlify

How to run the Mintlify docs preview locally from this repository.

Prerequisites

  • Node.js >=20.17.0

  • pnpm

  • Repository dependencies installed (pnpm install from repo root)

  • OpenAPI specs in openapi/ are committed to git. Hosted Mintlify cannot run monorepo codegen on deploy, so these files must be present in the repo for production docs.

    When you change API route OpenAPI metadata, regenerate and commit the four specs from the repo root:

    pnpm run --filter @stackframe/backend codegen-docs
    git add docs-mintlify/openapi/
    

    That writes client.json, server.json, admin.json, and webhooks.json into docs-mintlify/openapi/ (and into docs/openapi/ for the legacy Fumadocs app). CI fails if pnpm codegen produces different output than what is committed (see root lint-and-build workflow).

Run locally

From the repository root:

pnpm -C docs-mintlify run dev

This starts Mintlify in docs-mintlify on http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04 (for example, http://localhost:8104 with the default prefix).

From inside docs-mintlify, you can also run:

pnpm dev

Useful variants:

# Override the default port
pnpm -C docs-mintlify run dev -- --port 3333

# Skip OpenAPI processing for faster iteration
pnpm -C docs-mintlify run dev -- --disable-openapi

Search + assistant in local preview

If you want local search and the Mintlify assistant:

pnpm -C docs-mintlify run login
pnpm -C docs-mintlify run status

Then re-run pnpm -C docs-mintlify run dev.

Package scripts

From repo root:

pnpm -C docs-mintlify run lint
pnpm -C docs-mintlify run typecheck
pnpm -C docs-mintlify run build
pnpm -C docs-mintlify run clean

lint runs both mint validate and mint broken-links.