From 01b4ecba1a6fc385a21f897a2e7bf4edc0a66ce4 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 29 Sep 2025 22:40:53 -0500 Subject: [PATCH 01/55] Remove platform selection --- docs/docs-platform.yml | 295 ------------ docs/package.json | 6 +- docs/scripts/generate-docs.js | 451 ------------------ docs/scripts/generate-platform-navigation.js | 143 ------ docs/src/app/api/search/route.ts | 33 +- docs/src/app/docs/[[...slug]]/page.tsx | 20 +- docs/src/components/homepage/iconHover.tsx | 256 ++-------- .../layout/custom-search-dialog.tsx | 159 +----- docs/src/components/layout/root-toggle.tsx | 249 ---------- .../layouts/docs-header-wrapper.tsx | 68 +-- .../components/layouts/docs-layout-router.tsx | 178 +------ docs/src/components/layouts/docs.tsx | 309 +----------- .../layouts/platform-aware-header.tsx | 43 -- .../layouts/shared/section-utils.ts | 19 +- docs/src/components/platform-redirect.tsx | 98 ---- docs/src/components/sdk/overview.tsx | 13 +- docs/src/hooks/use-platform-persistence.ts | 47 -- docs/src/hooks/use-platform-preference.ts | 78 --- docs/src/lib/navigation-utils.ts | 106 ++-- docs/src/lib/platform-utils.ts | 37 -- package.json | 12 +- 21 files changed, 151 insertions(+), 2469 deletions(-) delete mode 100644 docs/docs-platform.yml delete mode 100644 docs/scripts/generate-docs.js delete mode 100644 docs/scripts/generate-platform-navigation.js delete mode 100644 docs/src/components/layout/root-toggle.tsx delete mode 100644 docs/src/components/layouts/platform-aware-header.tsx delete mode 100644 docs/src/components/platform-redirect.tsx delete mode 100644 docs/src/hooks/use-platform-persistence.ts delete mode 100644 docs/src/hooks/use-platform-preference.ts delete mode 100644 docs/src/lib/platform-utils.ts diff --git a/docs/docs-platform.yml b/docs/docs-platform.yml deleted file mode 100644 index cab9674b4c..0000000000 --- a/docs/docs-platform.yml +++ /dev/null @@ -1,295 +0,0 @@ -# Platform-specific content filtering configuration -# Explicit page-by-page listing approach -# -# NOTE: Changes to this file automatically update the smart platform navigation system. -# The src/lib/platform-navigation.ts file is auto-generated from this data during build. - -pages: - # Root pages - - path: overview.mdx - platforms: ["next", "react", "js", "python"] - - - path: faq.mdx - platforms: ["next", "react", "js", "python"] - - # SDK Reference - - path: sdk/index.mdx - platforms: ["next", "react", "js", "python"] - - # Components - - path: components/index.mdx - platforms: ["next", "react"] # Only React-like platforms - - # Getting Started - - path: getting-started/setup.mdx - platforms: ["next", "react", "js", "python"] - - - path: getting-started/components.mdx - platforms: ["next", "react"] # Only React-like platforms - - - path: getting-started/users.mdx - platforms: ["next", "react", "js"] # No Python - - - path: getting-started/example-pages.mdx - platforms: ["js"] # Only vanilla JS - - # Auth Providers - Available for all platforms since OAuth is universal - - path: concepts/auth-providers/index.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/github.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/google.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/facebook.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/microsoft.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/spotify.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/discord.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/gitlab.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/apple.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/bitbucket.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/linkedin.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/auth-providers/x-twitter.mdx - platforms: ["next", "react", "js", "python"] - - # Advanced auth methods - More frontend-focused - - path: getting-started/auth-providers/passkey.mdx - platforms: ["next", "react", "js"] # No Python (frontend feature) - - - path: getting-started/auth-providers/two-factor-auth.mdx - platforms: ["next", "react", "js"] # No Python (frontend feature) - - - path: getting-started/production.mdx - platforms: ["next", "react", "js"] # No Python - - # Concepts - - path: concepts/stack-app.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/custom-user-data.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/user-onboarding.mdx - platforms: ["next", "react"] # No JS or Python - - - path: concepts/oauth.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/orgs-and-teams.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/team-selection.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/permissions.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/api-keys.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/webhooks.mdx - platforms: ["next", "react", "js"] # No Python - - - path: concepts/backend-integration.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/jwt.mdx - platforms: ["next", "react", "js", "python"] - - - path: concepts/emails.mdx - platforms: ["next", "react", "js"] # No Python (server-side email functionality) - - # Components (React-like only) - - path: components/overview.mdx - platforms: ["next", "react"] - - - path: components/user-button.mdx - platforms: ["next", "react"] - - - path: components/selected-team-switcher.mdx - platforms: ["next", "react"] - - - path: components/account-settings.mdx - platforms: ["next", "react"] - - - path: components/sign-in.mdx - platforms: ["next", "react"] - - - path: components/sign-up.mdx - platforms: ["next", "react"] - - - path: components/credential-sign-in.mdx - platforms: ["next", "react"] - - - path: components/credential-sign-up.mdx - platforms: ["next", "react"] - - - path: components/magic-link-sign-in.mdx - platforms: ["next", "react"] - - - path: components/forgot-password.mdx - platforms: ["next", "react"] - - - path: components/password-reset.mdx - platforms: ["next", "react"] - - - path: components/oauth-button.mdx - platforms: ["next", "react"] - - - path: components/oauth-button-group.mdx - platforms: ["next", "react"] - - - path: components/stack-handler.mdx - platforms: ["next", "react"] - - - path: components/stack-provider.mdx - platforms: ["next", "react"] - - - path: components/stack-theme.mdx - platforms: ["next", "react"] - - # Customization (React-like only) - - path: customization/dark-mode.mdx - platforms: ["next", "react"] - - - path: customization/custom-styles.mdx - platforms: ["next", "react"] - - - path: customization/internationalization.mdx - platforms: ["next", "react"] - - - path: customization/custom-pages.mdx - platforms: ["next", "react"] - - - path: customization/page-examples/index.mdx - platforms: ["next", "react"] - - - path: customization/page-examples/sign-in.mdx - platforms: ["next", "react"] - - - path: customization/page-examples/sign-up.mdx - platforms: ["next", "react"] - - - path: customization/page-examples/forgot-password.mdx - platforms: ["next", "react"] - - - path: customization/page-examples/password-reset.mdx - platforms: ["next", "react"] - - # SDK Reference - - path: sdk/overview.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/overview-new.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/objects/stack-app.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/objects/stack-app-test.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/api-key.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/connected-account.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/contact-channel.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/project.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/team.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/team-permission.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/team-profile.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/team-user.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/user.mdx - platforms: ["next", "react", "js"] # No Python - - - path: sdk/types/email.mdx - platforms: ["next", "react", "js"] # No Python - - # SDK Hooks (React-like only) - - path: sdk/hooks/use-stack-app.mdx - platforms: ["next", "react"] # No JS or Python - - - path: sdk/hooks/use-user.mdx - platforms: ["next", "react"] # No JS or Python - - # Snippets (utility files - exclude from Python) - - path: snippets/always-tab-codeblock.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/big-divider.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/divider.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/get-user-or-parameter.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/make-full-page.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/stack-app-constructor-options-after-ssk.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/stack-app-constructor-options-before-ssk.mdx - platforms: ["next", "react", "js"] # No Python - - - path: snippets/use-on-server-callout.mdx - platforms: ["next", "react", "js"] # No Python - - # Others - - path: others/self-host.mdx - platforms: ["next", "react", "js", "python"] # All platforms - - - path: others/supabase.mdx - platforms: ["next"] # Next only - - - path: others/convex.mdx - platforms: ["next", "react", "js"] # No Python - - - path: others/cli-authentication.mdx - platforms: ["python"] # Python only - - # Python-specific content from templates-python/ - # Authentication section - - - path: concepts/user-authentication.mdx - platforms: ["python"] # python only - - - path: concepts/teams-management.mdx - platforms: ["python"] # python only - - - diff --git a/docs/package.json b/docs/package.json index e63c57135b..4cea5eb8da 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,15 +6,13 @@ "private": true, "type": "module", "scripts": { - "build": "npm run generate-platform-nav && next build", - "dev": "npm run generate-platform-nav && next dev --port 8104 --hostname 0.0.0.0", + "build": "next build", + "dev": "next dev --port 8104 --hostname 0.0.0.0", "start": "next start --port 8104", "lint": "next lint", "lint:fix": "next lint --fix", "postinstall": "fumadocs-mdx", - "generate-docs": "node scripts/generate-docs.js", "generate-openapi-docs": "node scripts/generate-functional-api-docs.mjs", - "generate-platform-nav": "node scripts/generate-platform-navigation.js", "setup-openapi": "node scripts/setup-openapi.mjs", "clear-docs": "node scripts/clear-docs.js" }, diff --git a/docs/scripts/generate-docs.js b/docs/scripts/generate-docs.js deleted file mode 100644 index 0ac0b34052..0000000000 --- a/docs/scripts/generate-docs.js +++ /dev/null @@ -1,451 +0,0 @@ -import fs from 'fs'; -import { glob } from 'glob'; -import yaml from 'js-yaml'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -// Get __dirname equivalent in ESM -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Configure paths -const TEMPLATE_DIR = path.resolve(__dirname, '../templates'); -const PYTHON_TEMPLATE_DIR = path.resolve(__dirname, '../templates-python'); -const OUTPUT_BASE_DIR = path.resolve(__dirname, '../content/docs'); -const CONFIG_FILE = path.resolve(__dirname, '../docs-platform.yml'); -const PLATFORMS = ['next', 'react', 'js', 'python']; - -// Platform groups mapping -const PLATFORM_GROUPS = { - 'react-like': ['next', 'react'], // Platforms that use React components - 'js-like': ['next', 'react', 'js'] // Platforms that use JavaScript SDK (includes React-based platforms) -}; - -// Load platform configuration -let platformConfig = {}; -try { - const configContent = fs.readFileSync(CONFIG_FILE, 'utf8'); - platformConfig = yaml.load(configContent); - console.log('Loaded platform configuration from docs-platform.yml'); -} catch (error) { - console.error('Failed to load platform configuration:', error.message); - console.log('Falling back to include all files for all platforms'); -} - -// Platform folder naming - now using root folders -function getFolderName(platform) { - return platform; // Use direct platform names instead of pages-{platform} -} - -// Platform display names -function getPlatformDisplayName(platform) { - const platformNames = { - 'next': 'Next.js', - 'react': 'React', - 'js': 'JavaScript', - 'python': 'Python' - }; - return platformNames[platform] || platform; -} - -// Platform-specific content markers - Updated regex to handle both syntaxes (with and without colon) -const PLATFORM_START_MARKER = /{\s*\/\*\s*IF_PLATFORM:?\s*([\w-]+)\s*\*\/\s*}/; -const PLATFORM_ELSE_MARKER = /{\s*\/\*\s*ELSE_IF_PLATFORM:?\s+([\w-]+)\s*\*\/\s*}/; -const PLATFORM_END_MARKER = /{\s*\/\*\s*END_PLATFORM\s*\*\/\s*}/; - -/** - * Check if a platform or platform group includes the target platform - */ -function isPlatformMatch(platformSpec, targetPlatform) { - // Direct platform match - if (platformSpec === targetPlatform) { - return true; - } - - // Platform group match - if (PLATFORM_GROUPS[platformSpec]) { - return PLATFORM_GROUPS[platformSpec].includes(targetPlatform); - } - - return false; -} - -/** - * Check if a file should be included for a specific platform - */ -function shouldIncludeFileForPlatform(platform, filePath) { - // If no configuration loaded, include everything - if (!platformConfig.pages) { - return true; - } - - // Find the page configuration for this file - const pageConfig = platformConfig.pages.find(page => page.path === filePath); - - // If no specific configuration found, exclude by default - if (!pageConfig) { - console.log(`No configuration found for ${filePath}, excluding by default`); - return false; - } - - // Check if the platform is in the allowed list - return pageConfig.platforms.includes(platform); -} - -/** - * Process a template file for a specific platform - */ -function processTemplateForPlatform(content, targetPlatform) { - const lines = content.split('\n'); - let result = []; - let currentPlatformSpec = null; - let isIncluding = true; - let platformSection = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Check for platform start - const startMatch = line.match(PLATFORM_START_MARKER); - if (startMatch) { - platformSection = true; - currentPlatformSpec = startMatch[1]; - isIncluding = isPlatformMatch(currentPlatformSpec, targetPlatform); - continue; - } - - // Check for platform else - const elseMatch = line.match(PLATFORM_ELSE_MARKER); - if (elseMatch && platformSection) { - currentPlatformSpec = elseMatch[1]; - isIncluding = isPlatformMatch(currentPlatformSpec, targetPlatform); - continue; - } - - // Check for platform end - const endMatch = line.match(PLATFORM_END_MARKER); - if (endMatch && platformSection) { - platformSection = false; - isIncluding = true; - continue; - } - - // Include the line if we're supposed to - if (isIncluding) { - result.push(line); - } - } - - return result.join('\n'); -} - -/** - * Generate meta.json files for Fumadocs navigation - */ -function generateMetaFiles() { - // Process meta.json files for each platform from templates - for (const platform of PLATFORMS) { - const folderName = getFolderName(platform); - const platformDisplayName = getPlatformDisplayName(platform); - - // For Python platform, prioritize Python-specific templates, but also include shared templates - const templateDir = (platform === 'python' && fs.existsSync(PYTHON_TEMPLATE_DIR)) ? PYTHON_TEMPLATE_DIR : TEMPLATE_DIR; - - // Find all meta.json files in the appropriate template directory - const metaFiles = glob.sync('**/meta.json', { cwd: templateDir }); - - // For Python, also get meta.json files from shared templates (excluding root meta.json to avoid conflicts) - let sharedMetaFiles = []; - if (platform === 'python' && fs.existsSync(PYTHON_TEMPLATE_DIR)) { - sharedMetaFiles = glob.sync('**/meta.json', { cwd: TEMPLATE_DIR }).filter(file => file !== 'meta.json'); - } - - // Process Python-specific meta files - for (const metaFile of metaFiles) { - const srcPath = path.join(templateDir, metaFile); - const destPath = path.join(OUTPUT_BASE_DIR, folderName, metaFile); - - // If this is a nested meta.json (not root), check if the folder should exist for this platform - if (metaFile !== 'meta.json') { - const folderPath = path.dirname(metaFile); - - // Check if any pages in this folder are included for this platform - const hasContentInFolder = platformConfig.pages && platformConfig.pages.some(configPage => - configPage.path.startsWith(`${folderPath}/`) && - configPage.platforms.includes(platform) - ); - - if (!hasContentInFolder) { - console.log(`Skipped meta.json for ${folderPath} (no content for ${platform})`); - continue; // Skip this meta.json file - } - } - - // Read and parse the template meta.json - const templateContent = fs.readFileSync(srcPath, 'utf8'); - const metaData = JSON.parse(templateContent); - - // If this is the root meta.json, customize it for the platform - if (metaFile === 'meta.json') { - metaData.title = platformDisplayName; - metaData.description = `Stack Auth for ${platformDisplayName} applications`; - metaData.root = true; - - // Filter pages based on platform configuration - if (platformConfig.pages && metaData.pages) { - const cleanedPages = []; - let currentSectionPages = []; - let currentSectionHeader = null; - - for (let i = 0; i < metaData.pages.length; i++) { - const page = metaData.pages[i]; - - // If this is a section divider - if (typeof page === 'string' && page.startsWith('---')) { - // Process the previous section first (or pages before first section) - if (currentSectionPages.length > 0) { - if (currentSectionHeader !== null) { - // Add section header if we had one - cleanedPages.push(currentSectionHeader); - } - cleanedPages.push(...currentSectionPages); - } - - // Start new section - currentSectionHeader = page; - currentSectionPages = []; - } - // If this is a folder reference (like "...customization") - else if (typeof page === 'string' && page.startsWith('...')) { - // Only include folder references if they have content for this platform - const folderName = page.substring(3); // Remove "..." - const hasContentInFolder = platformConfig.pages.some(configPage => - configPage.path.startsWith(`${folderName}/`) && - configPage.platforms.includes(platform) - ); - - if (hasContentInFolder) { - currentSectionPages.push(page); - } - } - // Regular page - else { - // Check if this is actually a folder reference vs a page reference - // Check both template directories for Python - let folderPath = path.join(templateDir, page); - let isActualFolder = fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory(); - - // For Python, also check shared templates directory - if (!isActualFolder && platform === 'python') { - folderPath = path.join(TEMPLATE_DIR, page); - isActualFolder = fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory(); - } - - if (isActualFolder) { - // This is a folder reference - check if folder has content for this platform - const hasContentInFolder = platformConfig.pages.some(configPage => - configPage.path.startsWith(`${page}/`) && - configPage.platforms.includes(platform) - ); - - if (hasContentInFolder) { - currentSectionPages.push(page); - } - } else { - // This is a regular page reference - const pagePath = `${page}.mdx`; - const shouldInclude = shouldIncludeFileForPlatform(platform, pagePath); - if (shouldInclude) { - currentSectionPages.push(page); - } - } - } - } - - // Don't forget the last section (or remaining pages) - if (currentSectionPages.length > 0) { - if (currentSectionHeader !== null) { - cleanedPages.push(currentSectionHeader); - } - cleanedPages.push(...currentSectionPages); - } - - metaData.pages = cleanedPages; - } - } - - // Create directory if it doesn't exist - fs.mkdirSync(path.dirname(destPath), { recursive: true }); - - // Write the processed meta.json - fs.writeFileSync(destPath, JSON.stringify(metaData, null, 2)); - console.log(`Generated platform-specific meta.json for ${platform}: ${destPath}`); - } - - // For Python, also process shared meta.json files (but not root) - for (const metaFile of sharedMetaFiles) { - const folderPath = path.dirname(metaFile); - - // Check if any pages in this folder are included for Python - const hasContentInFolder = platformConfig.pages && platformConfig.pages.some(configPage => - configPage.path.startsWith(`${folderPath}/`) && - configPage.platforms.includes(platform) - ); - - if (hasContentInFolder) { - const srcPath = path.join(TEMPLATE_DIR, metaFile); - const destPath = path.join(OUTPUT_BASE_DIR, folderName, metaFile); - - // Read and copy the shared meta.json - const templateContent = fs.readFileSync(srcPath, 'utf8'); - - // Create directory if it doesn't exist - fs.mkdirSync(path.dirname(destPath), { recursive: true }); - - // Write the shared meta.json - fs.writeFileSync(destPath, templateContent); - console.log(`Generated shared meta.json for ${platform}: ${destPath}`); - } - } - } -} - -/** - * Copy assets from template to platform-specific directories - */ -function copyAssets() { - const assetDirs = ['imgs']; - - // Copy assets from main templates directory - for (const dir of assetDirs) { - const srcDir = path.join(TEMPLATE_DIR, dir); - - if (fs.existsSync(srcDir)) { - // Copy assets to each platform directory - for (const platform of PLATFORMS) { - const folderName = getFolderName(platform); - const destDir = path.join(OUTPUT_BASE_DIR, folderName, dir); - fs.mkdirSync(destDir, { recursive: true }); - - // Find and copy all files - const files = glob.sync('**/*', { cwd: srcDir, nodir: true }); - for (const file of files) { - const srcFile = path.join(srcDir, file); - const destFile = path.join(destDir, file); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - fs.copyFileSync(srcFile, destFile); - console.log(`Copied asset: ${srcFile} -> ${destFile}`); - } - } - } - } - - // Copy Python-specific assets if they exist - if (fs.existsSync(PYTHON_TEMPLATE_DIR)) { - for (const dir of assetDirs) { - const srcDir = path.join(PYTHON_TEMPLATE_DIR, dir); - - if (fs.existsSync(srcDir)) { - const destDir = path.join(OUTPUT_BASE_DIR, 'python', dir); - fs.mkdirSync(destDir, { recursive: true }); - - // Find and copy all files - const files = glob.sync('**/*', { cwd: srcDir, nodir: true }); - for (const file of files) { - const srcFile = path.join(srcDir, file); - const destFile = path.join(destDir, file); - fs.mkdirSync(path.dirname(destFile), { recursive: true }); - fs.copyFileSync(srcFile, destFile); - console.log(`Copied Python-specific asset: ${srcFile} -> ${destFile}`); - } - } - } - } -} - -/** - * Main function to generate platform-specific docs - */ -function generateDocs() { - // Find all MDX files in the main template directory - const templateFiles = glob.sync('**/*.mdx', { cwd: TEMPLATE_DIR }); - - if (templateFiles.length === 0) { - console.warn(`No template files found in ${TEMPLATE_DIR}`); - return; - } - - console.log(`Found ${templateFiles.length} shared template files`); - - // Process shared templates for each platform - for (const platform of PLATFORMS) { - const folderName = getFolderName(platform); - const outputDir = path.join(OUTPUT_BASE_DIR, folderName); - - // Create the output directory - fs.mkdirSync(outputDir, { recursive: true }); - - // Process each shared template file - for (const file of templateFiles) { - // Check if this file should be included for this platform - if (!shouldIncludeFileForPlatform(platform, file)) { - console.log(`Skipped file (not configured for platform): ${file} for ${platform}`); - continue; - } - - const inputFile = path.join(TEMPLATE_DIR, file); - const outputFile = path.join(outputDir, file); - - // Read the template - const templateContent = fs.readFileSync(inputFile, 'utf8'); - - // Process for this platform - const processedContent = processTemplateForPlatform(templateContent, platform); - - // Create output directory if it doesn't exist - fs.mkdirSync(path.dirname(outputFile), { recursive: true }); - - // Write the processed content - fs.writeFileSync(outputFile, processedContent); - - console.log(`Generated: ${outputFile}`); - } - } - - // Process Python-specific templates if they exist - if (fs.existsSync(PYTHON_TEMPLATE_DIR)) { - console.log(`Processing Python-specific templates from ${PYTHON_TEMPLATE_DIR}`); - const pythonTemplateFiles = glob.sync('**/*.mdx', { cwd: PYTHON_TEMPLATE_DIR }); - - if (pythonTemplateFiles.length > 0) { - const pythonOutputDir = path.join(OUTPUT_BASE_DIR, 'python'); - - for (const file of pythonTemplateFiles) { - const inputFile = path.join(PYTHON_TEMPLATE_DIR, file); - const outputFile = path.join(pythonOutputDir, file); - - // Read the Python-specific template - const templateContent = fs.readFileSync(inputFile, 'utf8'); - - // Create output directory if it doesn't exist - fs.mkdirSync(path.dirname(outputFile), { recursive: true }); - - // Write the content (no platform processing needed for Python-specific files) - fs.writeFileSync(outputFile, templateContent); - - console.log(`Generated Python-specific: ${outputFile}`); - } - } - } - - // Generate meta.json files for navigation - generateMetaFiles(); - - // Copy assets (images, etc.) - copyAssets(); - - console.log('Documentation generation complete!'); -} - -// Run the generator -generateDocs(); diff --git a/docs/scripts/generate-platform-navigation.js b/docs/scripts/generate-platform-navigation.js deleted file mode 100644 index 80d3b9d5b2..0000000000 --- a/docs/scripts/generate-platform-navigation.js +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env node - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { parse } from 'yaml'; - -/** - * Generate platform-navigation.ts from docs-platform.yml - * This ensures we have a single source of truth for platform pages - */ - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const DOCS_PLATFORM_YML_PATH = path.join(__dirname, '..', 'docs-platform.yml'); -const OUTPUT_PATH = path.join(__dirname, '..', 'src', 'lib', 'platform-navigation.ts'); - -function generatePlatformNavigation() { - try { - // Read and parse docs-platform.yml - const yamlContent = fs.readFileSync(DOCS_PLATFORM_YML_PATH, 'utf8'); - const config = parse(yamlContent); - - if (!config.pages || !Array.isArray(config.pages)) { - throw new Error('Invalid docs-platform.yml format: missing pages array'); - } - - // Validate all platform values - const validPlatforms = new Set(['next', 'react', 'js', 'python']); // Actual platforms used in docs - config.pages.forEach((page, idx) => { - if (!Array.isArray(page.platforms)) { - throw new Error(`Invalid platforms array at page index ${idx}`); - } - page.platforms.forEach(platform => { - if (!validPlatforms.has(platform)) { - throw new Error(`Invalid platform "${platform}" at page index ${idx}`); - } - }); - }); - - // Generate TypeScript content - const tsContent = `// THIS FILE IS AUTO-GENERATED -// Do not edit manually - update docs-platform.yml instead -// Generated on: ${new Date().toISOString()} - -import { type Platform } from './platform-utils'; - -// Platform pages data auto-generated from docs-platform.yml -const PLATFORM_PAGES: { path: string, platforms: Platform[] }[] = [ -${config.pages.map(page => { - const platformsStr = JSON.stringify(page.platforms); - return ` { path: ${JSON.stringify(page.path)}, platforms: ${platformsStr} },`; -}).join('\n')} -]; - -/** - * Safely join URL path segments - */ -function joinUrlPath(...segments: string[]): string { - return segments - .filter(segment => segment && segment.length > 0) - .join('/') - .replace(/\\/+/g, '/'); // Remove duplicate slashes -} - -/** - * Check if a specific page exists for a given platform - */ -export function pageExistsForPlatform(path: string, platform: Platform): boolean { - // Normalize path - remove leading slash and ensure .mdx extension - const normalizedPath = path.replace(/^\\//, ''); - const pathWithExt = normalizedPath.endsWith('.mdx') ? normalizedPath : \`\${normalizedPath}.mdx\`; - - // First try to find exact match - let page = PLATFORM_PAGES.find(p => p.path === pathWithExt); - - // If not found and path doesn't end with index, try appending /index.mdx - if (!page && !pathWithExt.includes('/index.mdx')) { - const indexPath = normalizedPath.endsWith('.mdx') - ? normalizedPath.replace('.mdx', '/index.mdx') - : \`\${normalizedPath}/index.mdx\`; - page = PLATFORM_PAGES.find(p => p.path === indexPath); - } - - return page?.platforms.includes(platform) ?? false; -} - -/** - * Get smart redirect URL for platform switching - * If the current page doesn't exist for the target platform, redirect to overview - */ -export function getSmartPlatformRedirect(currentPath: string, targetPlatform: Platform): string { - // Remove /docs/{platform}/ prefix and normalize - const pathWithoutPlatform = currentPath.replace(/^\\/docs\\/[^\\/]+\\//, ''); - - // If the exact same page exists for target platform, use it - if (pageExistsForPlatform(pathWithoutPlatform, targetPlatform)) { - const cleanPath = pathWithoutPlatform.replace(/\\.mdx$/, ''); - return joinUrlPath('/docs', targetPlatform, cleanPath); - } - - // Otherwise, redirect to overview - return joinUrlPath('/docs', targetPlatform, 'overview'); -} - -/** - * Get all pages available for a specific platform - */ -export function getPagesForPlatform(platform: Platform): string[] { - return PLATFORM_PAGES - .filter(page => page.platforms.includes(platform)) - .map(page => page.path.replace(/\\.mdx$/, '')); -} -`; - - // Write the generated file - fs.writeFileSync(OUTPUT_PATH, tsContent, 'utf8'); - - console.log('✅ Generated platform-navigation.ts from docs-platform.yml'); - console.log(`📄 ${config.pages.length} pages configured for platforms`); - - // Show platform distribution - const platformCounts = {}; - config.pages.forEach(page => { - page.platforms.forEach(platform => { - platformCounts[platform] = (platformCounts[platform] || 0) + 1; - }); - }); - - console.log('📊 Platform distribution:'); - Object.entries(platformCounts).forEach(([platform, count]) => { - console.log(` ${platform}: ${count} pages`); - }); - - } catch (error) { - console.error('❌ Error generating platform-navigation.ts:', error.message); - process.exit(1); - } -} - -// Run the generator -generatePlatformNavigation(); diff --git a/docs/src/app/api/search/route.ts b/docs/src/app/api/search/route.ts index 884b2a1c37..4f99ebba51 100644 --- a/docs/src/app/api/search/route.ts +++ b/docs/src/app/api/search/route.ts @@ -11,18 +11,6 @@ type SearchResult = { score: number, // Add scoring for prioritization }; -// Helper function to get platform priority for tie-breaking -function getPlatformPriority(url: string): number { - // Higher number = higher priority - if (url.includes('/docs/next/')) return 100; - if (url.includes('/docs/react/')) return 90; - if (url.includes('/docs/js/')) return 80; - if (url.includes('/docs/python/')) return 70; - // API and other pages - if (url.includes('/api/')) return 60; - return 50; // Default priority -} - // Helper function to calculate search relevance score function calculateScore(query: string, text: string, type: 'title' | 'description' | 'heading' | 'content'): number { const queryLower = query.toLowerCase().trim(); @@ -202,18 +190,11 @@ export async function GET(request: NextRequest) { }); // Sort results by score in descending order (highest score first) - // Use platform priority as tie-breaker when scores are equal - results.sort((a, b) => { - if (b.score !== a.score) { - return b.score - a.score; // Primary sort by score - } - return getPlatformPriority(b.url) - getPlatformPriority(a.url); // Tie-breaker by platform priority - }); + results.sort((a, b) => b.score - a.score); console.log(`\n=== RAW RESULTS FOR "${query}" ===`); results.slice(0, 10).forEach((result, i) => { - const priority = getPlatformPriority(result.url); - console.log(`${i + 1}. "${result.content}" (${result.type}) - Score: ${result.score.toFixed(1)} - Priority: ${priority} - URL: ${result.url}`); + console.log(`${i + 1}. "${result.content}" (${result.type}) - Score: ${result.score.toFixed(1)} - URL: ${result.url}`); }); // Remove duplicate URLs and keep only the highest scoring result per URL @@ -229,17 +210,11 @@ export async function GET(request: NextRequest) { }); // Re-sort after deduplication using the same logic - uniqueResults.sort((a, b) => { - if (b.score !== a.score) { - return b.score - a.score; // Primary sort by score - } - return getPlatformPriority(b.url) - getPlatformPriority(a.url); // Tie-breaker by platform priority - }); + uniqueResults.sort((a, b) => b.score - a.score); console.log(`\n=== FINAL RESULTS FOR "${query}" ===`); uniqueResults.slice(0, 10).forEach((result, i) => { - const priority = getPlatformPriority(result.url); - console.log(`${i + 1}. "${result.content}" (${result.type}) - Score: ${result.score.toFixed(1)} - Priority: ${priority} - URL: ${result.url}`); + console.log(`${i + 1}. "${result.content}" (${result.type}) - Score: ${result.score.toFixed(1)} - URL: ${result.url}`); }); console.log(`\nFound ${uniqueResults.length} unique search results for "${query}"`); diff --git a/docs/src/app/docs/[[...slug]]/page.tsx b/docs/src/app/docs/[[...slug]]/page.tsx index 0f4d56322b..1f40b2613c 100644 --- a/docs/src/app/docs/[[...slug]]/page.tsx +++ b/docs/src/app/docs/[[...slug]]/page.tsx @@ -11,6 +11,19 @@ import { source } from 'lib/source'; import { Metadata } from 'next'; import { redirect } from 'next/navigation'; +function getDefaultDocsRedirectUrl(): string | null { + const pages = source.getPages(); + // Prefer an overview page if one exists without platform prefix + const overviewPage = pages.find(page => page.url === '/docs/overview'); + if (overviewPage) { + return overviewPage.url; + } + + // Fall back to the first docs page in the collection + const firstDocsPage = pages.find(page => page.url.startsWith('/docs/')); + return firstDocsPage?.url ?? null; +} + export default async function Page(props: { params: Promise<{ slug?: string[] }>, }) { @@ -18,7 +31,12 @@ export default async function Page(props: { // Handle redirect when no slug is provided (i.e., accessing /docs directly) if (!params.slug || params.slug.length === 0) { - redirect("/docs/next/overview"); + const fallbackUrl = getDefaultDocsRedirectUrl(); + if (fallbackUrl) { + redirect(fallbackUrl); + } + + redirect("/"); } const page = source.getPage(params.slug); diff --git a/docs/src/components/homepage/iconHover.tsx b/docs/src/components/homepage/iconHover.tsx index b9a89bdf65..cbbbe64af0 100644 --- a/docs/src/components/homepage/iconHover.tsx +++ b/docs/src/components/homepage/iconHover.tsx @@ -1,10 +1,7 @@ "use client"; -import { Book, ChevronDown, Code, Command, Layers, Search, Zap } from "lucide-react"; -import React, { useEffect, useRef, useState } from "react"; -import { usePlatformPreference } from "../../hooks/use-platform-preference"; -import { platformSupportsComponents, platformSupportsSDK } from "../../lib/navigation-utils"; -import { PLATFORMS, type Platform } from "../../lib/platform-utils"; +import { Book, Code, Command, Layers, Search, Zap } from "lucide-react"; +import React, { useState } from "react"; type DocsSection = { id: string, @@ -18,206 +15,48 @@ type DocsSection = { type DocsIcon3DProps = { sections?: DocsSection[], onSectionSelect?: (section: DocsSection) => void, - selectedPlatform?: Platform, } - -const createPlatformSections = (platform: Platform): DocsSection[] => { - const sections: DocsSection[] = [ - { - id: "guides", - title: "Guides", - description: "Complete guides and tutorials", - icon: , - url: `/docs/${platform}/overview`, - color: "rgb(59, 130, 246)", - }, - ]; - - // Add SDK if platform supports it - if (platformSupportsSDK(platform)) { - sections.push({ - id: "sdks", - title: "SDKs", - description: "Software development kits", - icon: , - url: `/docs/${platform}/sdk`, - color: "rgb(16, 185, 129)", - }); - } - - // Add Components if platform supports it - if (platformSupportsComponents(platform)) { - sections.push({ - id: "components", - title: "Components", - description: "Reusable UI components", - icon: , - url: `/docs/${platform}/components`, - color: "rgb(245, 101, 101)", - }); - } - - // Always add API (platform agnostic) - sections.push({ +const DEFAULT_SECTIONS: DocsSection[] = [ + { + id: "guides", + title: "Guides", + description: "Complete guides and tutorials", + icon: , + url: "/docs/overview", + color: "rgb(59, 130, 246)", + }, + { + id: "sdk", + title: "SDK", + description: "Software development kits", + icon: , + url: "/docs/sdk", + color: "rgb(16, 185, 129)", + }, + { + id: "components", + title: "Components", + description: "Reusable UI components", + icon: , + url: "/docs/components", + color: "rgb(245, 101, 101)", + }, + { id: "api", title: "API Reference", description: "Complete API documentation", icon: , url: "/api/overview", color: "rgb(168, 85, 247)", - }); - - return sections; -}; - -const PlatformSelector: React.FC<{ - selectedPlatform: Platform, - onPlatformChange: (platform: Platform) => void, -}> = ({ selectedPlatform, onPlatformChange }) => { - const [isOpen, setIsOpen] = useState(false); - const [hoveredPlatform, setHoveredPlatform] = useState(null); - const dropdownRef = useRef(null); - - const platformNames: Record = { - next: "Next.js", - react: "React", - js: "JavaScript", - python: "Python", - }; - - const platformColors: Record = { - next: "rgb(59, 130, 246)", // Blue - react: "rgb(16, 185, 129)", // Green - js: "rgb(245, 158, 11)", // Yellow - python: "rgb(168, 85, 247)", // Purple - }; - - // Click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); - setHoveredPlatform(null); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - return ( -
-
-

Choose Your Platform

-

Select your development environment

-
- -
- - - {isOpen && ( -
- {PLATFORMS.map((platform) => { - const isSelected = selectedPlatform === platform; - const isHovered = hoveredPlatform === platform; - const isHighlighted = isSelected || isHovered; - - return ( - - ); - })} -
- )} -
-
- ); -}; + }, +]; const DocsIcon3D: React.FC = ({ - sections, + sections = DEFAULT_SECTIONS, onSectionSelect, - selectedPlatform = "next" }) => { const [hoveredSection, setHoveredSection] = useState(null); - // Use platform-based sections if none provided - const platformSections = sections || createPlatformSections(selectedPlatform); - const handleSectionClick = (section: DocsSection) => { if (onSectionSelect) { onSectionSelect(section); @@ -240,13 +79,13 @@ const DocsIcon3D: React.FC = ({
= 4 ? 'grid-cols-2 md:grid-cols-4' : ''} `} > - {platformSections.map((section) => ( + {sections.map((section) => (
= ({ }; export default function DocsSelector() { - const { preferredPlatform, setPreferredPlatform, isLoaded } = usePlatformPreference(); - const [selectedPlatform, setSelectedPlatform] = useState(preferredPlatform); - - // Update selected platform when preference loads - useEffect(() => { - if (isLoaded) { - setSelectedPlatform(preferredPlatform); - } - }, [preferredPlatform, isLoaded]); - const handleSectionSelect = (section: DocsSection) => { //console.log("Selected section:", section); // Navigate to the selected section @@ -325,12 +154,6 @@ export default function DocsSelector() { } }; - const handlePlatformChange = (platform: Platform) => { - setSelectedPlatform(platform); - // Also update the preference in localStorage - setPreferredPlatform(platform); - }; - // Simple search button that opens the shared search dialog const handleSearchOpen = () => { // Trigger the main search dialog by dispatching the Cmd+K event @@ -344,11 +167,6 @@ export default function DocsSelector() { return (
- - {/* Search Bar - uses shared search dialog */}
@@ -374,7 +192,7 @@ export default function DocsSelector() {
diff --git a/docs/src/components/layout/custom-search-dialog.tsx b/docs/src/components/layout/custom-search-dialog.tsx index 8d3ad7aa47..08749c8004 100644 --- a/docs/src/components/layout/custom-search-dialog.tsx +++ b/docs/src/components/layout/custom-search-dialog.tsx @@ -1,29 +1,10 @@ 'use client'; -import { AlignLeft, ChevronDown, ExternalLink, FileText, Hash, Search, X } from 'lucide-react'; +import { AlignLeft, ExternalLink, FileText, Hash, Search, X } from 'lucide-react'; import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { cn } from '../../lib/cn'; -// Platform colors matching your theme -const PLATFORM_COLORS = { - 'next': '#3B82F6', // Blue - matches rgb(59, 130, 246) - 'react': '#10B981', // Green - matches rgb(16, 185, 129) - 'js': '#F59E0B', // Yellow - matches rgb(245, 158, 11) - 'javascript': '#F59E0B', // Yellow - matches rgb(245, 158, 11) - 'python': '#A855F7', // Purple - matches rgb(168, 85, 247) - 'api': '#FF6B6B', // Keep existing red for API -} as const; - -const PLATFORM_NAMES = { - 'next': 'Next.js', - 'react': 'React', - 'js': 'JavaScript', - 'javascript': 'JavaScript', - 'python': 'Python', - 'api': 'API', -} as const; - type SearchResult = { id: string, type: 'page' | 'heading' | 'text', @@ -32,21 +13,14 @@ type SearchResult = { }; type GroupedResult = { - platform: string, basePath: string, title: string, results: SearchResult[], }; -function extractPlatformFromUrl(url: string): string { - const match = url.match(/\/docs\/([^\/]+)/); - const platform = match?.[1] || 'api'; - return platform; -} - function extractBasePathFromUrl(url: string): string { // Extract everything after the platform but before any hash - const match = url.match(/\/docs\/[^\/]+(.+?)(?:#|$)/); + const match = url.match(/\/docs\/(?:[^\/]+\/)?(.+?)(?:#|$)/); return match?.[1] || ''; } @@ -55,9 +29,8 @@ function groupResultsByPage(results: SearchResult[]): GroupedResult[] { const groupOrder: string[] = []; // Track the order groups are first encountered for (const result of results) { - const platform = extractPlatformFromUrl(result.url); const basePath = extractBasePathFromUrl(result.url); - const baseUrl = `/docs/${platform}${basePath}`; + const baseUrl = result.url.split('#')[0]; if (!grouped.has(baseUrl)) { // Find the page title from page-type results, fallback to path-based title @@ -65,7 +38,6 @@ function groupResultsByPage(results: SearchResult[]): GroupedResult[] { const title = pageResult?.content || basePath.split('/').pop()?.replace(/-/g, ' ') || 'Unknown'; grouped.set(baseUrl, { - platform, basePath, title, results: [] @@ -86,24 +58,6 @@ function groupResultsByPage(results: SearchResult[]): GroupedResult[] { return groupOrder.map(url => grouped.get(url)!); } -function PlatformBadge({ platform }: { platform: string }) { - const color = PLATFORM_COLORS[platform as keyof typeof PLATFORM_COLORS]; - const name = PLATFORM_NAMES[platform as keyof typeof PLATFORM_NAMES]; - - return ( - - {name} - - ); -} - function SearchResultIcon({ type }: { type: string }) { switch (type) { case 'page': { @@ -130,34 +84,11 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); - const [selectedPlatformFilter, setSelectedPlatformFilter] = useState('all'); const [selectedIndex, setSelectedIndex] = useState(0); - const [dropdownOpen, setDropdownOpen] = useState(false); const inputRef = useRef(null); - const dropdownRef = useRef(null); const searchTimeoutRef = useRef(); - // Available platforms for the dropdown - const availablePlatforms = ['all', 'next', 'react', 'js', 'python', 'api']; - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setDropdownOpen(false); - } - }; - - if (dropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [dropdownOpen]); - const performSearch = useCallback(async (searchQuery: string) => { if (!searchQuery.trim()) { setResults([]); @@ -205,47 +136,36 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro const groupedResults = groupResultsByPage(results); // Filter by selected platform - const filteredResults = selectedPlatformFilter === 'all' - ? groupedResults - : groupedResults.filter(group => group.platform === selectedPlatformFilter); + const filteredResults = groupedResults; // Flatten results for keyboard navigation const flatResults = filteredResults.flatMap(group => group.results.map(result => ({ ...result, groupTitle: group.title, - platform: group.platform })) ); const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Escape': { - if (dropdownOpen) { - setDropdownOpen(false); - } else { - onOpenChange(false); - } + onOpenChange(false); break; } case 'ArrowDown': { - if (!dropdownOpen) { - e.preventDefault(); - setSelectedIndex(prev => Math.min(prev + 1, flatResults.length - 1)); - } + e.preventDefault(); + setSelectedIndex(prev => Math.min(prev + 1, flatResults.length - 1)); break; } case 'ArrowUp': { - if (!dropdownOpen) { - e.preventDefault(); - setSelectedIndex(prev => Math.max(prev - 1, 0)); - } + e.preventDefault(); + setSelectedIndex(prev => Math.max(prev - 1, 0)); break; } case 'Enter': { - if (!dropdownOpen) { - e.preventDefault(); - const selectedResult = flatResults[selectedIndex]; + e.preventDefault(); + const selectedResult = flatResults.at(selectedIndex); + if (selectedResult) { window.location.href = selectedResult.url; onOpenChange(false); } @@ -267,7 +187,6 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro setQuery(''); setResults([]); setSelectedIndex(0); - setDropdownOpen(false); } }, [open]); @@ -293,52 +212,6 @@ export function CustomSearchDialog({ open, onOpenChange }: CustomSearchDialogPro placeholder="Search documentation..." className="flex-1 px-0 py-4 text-sm bg-transparent outline-none placeholder:text-fd-muted-foreground" /> -
- - - {/* Dropdown Menu */} - {dropdownOpen && ( -
- {availablePlatforms.map((platform) => ( - - ))} -
- )} -
- - {open && ( -
- {options.map((item) => { - const isSelected = selected === item; - const isHovered = hoveredOption === item; - const isHighlighted = isSelected || isHovered; - const itemColor = getPlatformColor(item.title); - - return ( - { - setOpen(false); - setHoveredOption(null); - - // Store the platform preference when user clicks on a platform - const platform = extractPlatformFromUrl(item.url); - if (platform) { - setPreferredPlatform(platform); - } - }} - onMouseEnter={() => setHoveredOption(item)} - onMouseLeave={() => setHoveredOption(null)} - {...item.props} - className={cn( - 'w-full px-4 py-3 text-left transition-all duration-200 border-l-4 border-transparent block', - isHighlighted ? 'bg-fd-muted/70' : 'hover:bg-fd-muted/30', - item.props?.className, - )} - style={{ - borderLeftColor: isHighlighted ? itemColor : 'transparent', - backgroundColor: isHighlighted ? `${itemColor}15` : undefined, - }} - > -
-
- {item.icon} -
-

- {item.title} -

- {item.description && ( -

- {item.description} -

- )} -
-
- {isSelected && ( -
- )} - {isHovered && !isSelected && ( -
- )} -
- - ); - })} -
- )} -
-
- ); -} diff --git a/docs/src/components/layouts/docs-header-wrapper.tsx b/docs/src/components/layouts/docs-header-wrapper.tsx index f924d2e40d..30e5329b2e 100644 --- a/docs/src/components/layouts/docs-header-wrapper.tsx +++ b/docs/src/components/layouts/docs-header-wrapper.tsx @@ -3,9 +3,9 @@ import type { PageTree } from 'fumadocs-core/server'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { usePathname } from 'next/navigation'; import React, { useMemo, useState } from 'react'; -import { getCurrentPlatform } from '../../lib/platform-utils'; +import { generateNavLinks } from '@/lib/navigation-utils'; import { ApiSidebarContent } from './api/api-sidebar'; -import { PlatformAwareHeader } from './platform-aware-header'; +import { SharedHeader } from './shared-header'; import { isInApiSection } from './shared-header'; // Types for the page data @@ -179,7 +179,7 @@ function MobileClickableCollapsibleSection({ } // Recursive component to render page tree items for mobile -function MobilePageTreeItem({ item, currentPlatform }: { item: PageTree.Node, currentPlatform?: string }) { +function MobilePageTreeItem({ item }: { item: PageTree.Node }) { const pathname = usePathname(); if (item.type === 'separator') { @@ -201,7 +201,7 @@ function MobilePageTreeItem({ item, currentPlatform }: { item: PageTree.Node, cu defaultOpen={!!isCurrentPath} > {item.children.map((child, index) => ( - + ))} ); @@ -214,7 +214,7 @@ function MobilePageTreeItem({ item, currentPlatform }: { item: PageTree.Node, cu defaultOpen={!!isCurrentPath} > {item.children.map((child, index) => ( - + ))} ); @@ -227,57 +227,13 @@ function MobilePageTreeItem({ item, currentPlatform }: { item: PageTree.Node, cu ); } -// Function to find platform-specific content in the page tree -function findPlatformContent(tree: PageTree.Root, platform: string): PageTree.Node[] { - // Platform folder name mappings - const platformMappings: Record = { - 'next': ['next.js', 'nextjs'], - 'react': ['react'], - 'js': ['javascript'], - 'python': ['python'] - }; +function GeneralDocsSidebarContent({ pageTree }: { pageTree?: PageTree.Root }) { + if (!pageTree) return null; - const platformKey = platform.toLowerCase(); - const possibleNames = platformKey in platformMappings ? platformMappings[platformKey] : [platformKey]; - - for (const item of tree.children) { - if (item.type === 'folder') { - const itemName = String(item.name).toLowerCase(); - - for (const name of possibleNames) { - if (itemName.includes(name)) { - return item.children; - } - } - } - } - - return []; -} - -// Improved general docs sidebar content that renders the platform content -function GeneralDocsSidebarContent({ pageTree, pathname }: { pageTree?: PageTree.Root, pathname: string }) { - const currentPlatform = getCurrentPlatform(pathname); - - if (!currentPlatform || !pageTree) return null; - - // For platform-specific docs, show the platform folder's content - const platformContent = findPlatformContent(pageTree, currentPlatform); - if (platformContent.length > 0) { - return ( - <> - {platformContent.map((item, index) => ( - - ))} - - ); - } - - // For general docs or when no platform content found, show root level content return ( <> {pageTree.children.map((item, index) => ( - + ))} ); @@ -286,13 +242,14 @@ function GeneralDocsSidebarContent({ pageTree, pathname }: { pageTree?: PageTree /** * CLIENT-SIDE HEADER WRAPPER * - * This component wraps the PlatformAwareHeader and dynamically provides + * This component wraps the shared header and dynamically provides * sidebar content based on the current route. It's a client component * that can use hooks to determine the current section and provide * appropriate sidebar content for mobile navigation. */ export function DocsHeaderWrapper({ showSearch = true, pageTree, className, apiPages }: DocsHeaderWrapperProps) { const pathname = usePathname(); + const navLinks = useMemo(() => generateNavLinks(), []); // Determine current sidebar content based on route const sidebarContent = useMemo(() => { @@ -302,14 +259,15 @@ export function DocsHeaderWrapper({ showSearch = true, pageTree, className, apiP // For all docs pages, use the page tree if (pathname.startsWith('/docs') && !isInApiSection(pathname)) { - return ; + return ; } return null; }, [pathname, pageTree, apiPages]); return ( - -// Helper function to check if we're in SDK section -function isInSdkSection(pathname: string): boolean { - // Match the actual SDK section: /docs/platform/sdk or /docs/platform/sdk/... - // This excludes docs pages that might mention SDK in other contexts - const match = pathname.match(/^\/docs\/[^\/]+\/sdk($|\/)/); - return Boolean(match); -} - -// Helper function to check if we're in Components section -function isInComponentsSection(pathname: string): boolean { - // Match the actual Components section: /docs/platform/components or /docs/platform/components/... - // This excludes docs pages like /docs/platform/getting-started/components - const match = pathname.match(/^\/docs\/[^\/]+\/components($|\/)/); - return Boolean(match); -} - -// Helper function to find and extract a specific section from the page tree -function findSectionInTree(tree: PageTree.Root, sectionName: string, pathname: string): PageTree.Root | null { - // Look for the section in the current platform's content - const currentPlatform = getCurrentPlatform(pathname); - if (!currentPlatform) return null; - - // Platform name mappings - const platformMappings: Record = { - 'next': 'Next.js', - 'react': 'React', - 'js': 'JavaScript', - 'python': 'Python' - }; - - const platformDisplayName = platformMappings[currentPlatform]; - if (!platformDisplayName) return null; - - // Find the platform folder - const platformFolder = tree.children.find(node => - node.type === 'folder' && node.name === platformDisplayName - ); - - if (platformFolder && platformFolder.type === 'folder') { - // Look for the section within the platform folder - const sectionFolder = platformFolder.children.find(node => - node.type === 'folder' && node.name === sectionName - ); - - if (sectionFolder && sectionFolder.type === 'folder') { - return { - name: sectionFolder.name, - children: sectionFolder.children, - $id: tree.$id ? `${tree.$id}/${sectionName}` : sectionName, - }; - } - } - - return null; -} - export function DynamicDocsLayout({ children, ...props }: DynamicDocsLayoutProps) { const pathname = usePathname(); - // Determine which tree to use based on the current section - const pageTree = useMemo(() => { - if (isInSdkSection(pathname)) { - const sdkTree = findSectionInTree(props.tree, 'SDK Reference', pathname); - if (sdkTree) { - //console.log('🎯 Using SDK tree for:', pathname); - return sdkTree; - } - } - - if (isInComponentsSection(pathname)) { - const componentsTree = findSectionInTree(props.tree, 'Components', pathname); - if (componentsTree) { - //console.log('🎯 Using Components tree for:', pathname); - return componentsTree; - } - } - - // For normal docs view, filter out SDK and Components sections - //console.log('📄 Using filtered page tree for:', pathname); - return { - ...props.tree, - children: props.tree.children.map(platformNode => { - if (platformNode.type === 'folder') { - return { - ...platformNode, - children: platformNode.children.filter(node => { - // Hide SDK Reference and Components sections from normal docs - if (node.type === 'folder' && (node.name === 'SDK Reference' || node.name === 'Components')) { - return false; - } - return true; - }) - }; - } - return platformNode; - }) - }; - }, [pathname, props.tree]); - - const platformOptions: Option[] = useMemo(() => { - // Extract current platform from pathname - const currentPlatform = getCurrentPlatform(pathname); - - // Helper function to safely cast platform to Platform type - const isPlatform = (platform: string): platform is Platform => { - return ['next', 'react', 'js', 'python'].includes(platform); - }; - - return PLATFORMS.map(platform => { - // Safe type guard - if this fails, something is seriously wrong with PLATFORMS constant - if (!isPlatform(platform)) { - console.error(`Invalid platform in PLATFORMS array: ${platform}`); - // Fallback to a safe default to prevent runtime errors - platform = 'next'; - } - - const platformType = platform as Platform; - let url: string; - - if (isInSdkSection(pathname)) { - // For SDK section: check if platform supports SDK, otherwise use smart redirect - if (platformSupportsSDK(platformType)) { - url = `/docs/${platform}/sdk`; - } else { - url = getSmartRedirectUrl(pathname, platformType); - } - } else if (isInComponentsSection(pathname)) { - // For Components section: check if platform supports components, otherwise use smart redirect - if (platformSupportsComponents(platformType)) { - url = `/docs/${platform}/components`; - } else { - url = getSmartRedirectUrl(pathname, platformType); - } - } else { - // For normal docs: use smart redirect - url = getSmartRedirectUrl(pathname, platformType); - } - - return { - url, - title: getPlatformDisplayName(platform), - // Add urls set for more precise matching if this is the current platform - ...(platform === currentPlatform && { - urls: new Set([pathname]) - }) - }; - }); - }, [pathname]); - - // Auto-redirect to current platform if needed - if (pathname === '/docs' || pathname === '/docs/') { - return ; - } - // For API docs, use minimal layout without platform tabs if (isInApiSection(pathname)) { return ( null, @@ -238,14 +77,14 @@ export function DynamicDocsLayout({ children, ...props }: DynamicDocsLayoutProps {children} @@ -253,20 +92,19 @@ export function DynamicDocsLayout({ children, ...props }: DynamicDocsLayoutProps ); } - // For all other sections, use the standard layout with platform tabs - // The pageTree will be filtered for SDK/Components sections automatically + // For all other sections, use the standard layout without platform tabs return ( {children} diff --git a/docs/src/components/layouts/docs.tsx b/docs/src/components/layouts/docs.tsx index 3863b49991..b270c493d9 100644 --- a/docs/src/components/layouts/docs.tsx +++ b/docs/src/components/layouts/docs.tsx @@ -43,14 +43,10 @@ import { import { TreeContextProvider } from 'fumadocs-ui/contexts/tree'; import { ArrowLeft, ChevronDown, ChevronRight, Languages, Sidebar as SidebarIcon } from 'lucide-react'; import { usePathname, useRouter } from 'next/navigation'; -import { createContext, useContext, useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { CodeOverlayProvider, useCodeOverlay } from '../../hooks/use-code-overlay'; -import { usePlatformPreference } from '../../hooks/use-platform-preference'; import { cn } from '../../lib/cn'; -import { getSmartRedirectUrl } from '../../lib/navigation-utils'; -import { getSmartPlatformRedirect } from '../../lib/platform-navigation'; -import { getCurrentPlatform, type Platform } from '../../lib/platform-utils'; import { AIChatDrawer } from '../chat/ai-chat'; import { CustomSearchDialog } from '../layout/custom-search-dialog'; import { @@ -60,7 +56,6 @@ import { LanguageToggle, LanguageToggleText, } from '../layout/language-toggle'; -import { RootToggle } from '../layout/root-toggle'; import { ThemeToggle } from '../layout/theme-toggle'; import { DynamicCodeblockOverlay } from '../mdx/dynamic-code-block-overlay'; import { buttonVariants } from '../ui/button'; @@ -72,7 +67,6 @@ import { NavbarSidebarTrigger, } from './docs-client'; import { - getSidebarTabsFromOptions, layoutVariables, type SidebarOptions } from './docs/shared'; @@ -266,37 +260,8 @@ function ClickableCollapsibleSection({ ); } -// Function to find platform-specific content in the page tree -function findPlatformContent(tree: PageTree.Root, platform: string): PageTree.Node[] { - // Platform folder name mappings - const platformMappings: Record = { - 'next': ['next.js', 'nextjs'], - 'react': ['react'], - 'js': ['javascript'], - 'python': ['python'] - }; - - const platformKey = platform.toLowerCase(); - const possibleNames = platformKey in platformMappings ? platformMappings[platformKey] : [platformKey]; - - for (const item of tree.children) { - if (item.type === 'folder') { - const itemName = String(item.name).toLowerCase(); - - if (possibleNames.some(name => { - const normalizedName = name.trim().toLowerCase(); - return itemName === normalizedName || itemName.includes(normalizedName); - })) { - return item.children; - } - } - } - - return []; -} - // Recursive component to render page tree items with API styling -function PageTreeItem({ item, currentPlatform }: { item: PageTree.Node, currentPlatform?: string }) { +function PageTreeItem({ item }: { item: PageTree.Node }) { const pathname = usePathname(); if (item.type === 'separator') { @@ -318,7 +283,7 @@ function PageTreeItem({ item, currentPlatform }: { item: PageTree.Node, currentP defaultOpen={!!isCurrentPath} > {item.children.map((child, index) => ( - + ))} ); @@ -331,7 +296,7 @@ function PageTreeItem({ item, currentPlatform }: { item: PageTree.Node, currentP defaultOpen={!!isCurrentPath} > {item.children.map((child, index) => ( - + ))} ); @@ -346,204 +311,26 @@ function PageTreeItem({ item, currentPlatform }: { item: PageTree.Node, currentP // Function to render sidebar content based on context function renderSidebarContent(tree: PageTree.Root, pathname: string) { - const currentPlatform = getCurrentPlatform(pathname) || undefined; - - // For API section, don't show anything (API has its own sidebar) if (isInApiSection(pathname)) { return null; } - // For platform-specific docs, show the platform folder's content - if (currentPlatform) { - const platformContent = findPlatformContent(tree, currentPlatform); - if (platformContent.length > 0) { - return ( - <> - {platformContent.map((item, index) => ( - - ))} - - ); - } - } - - // For general docs or when no platform content found, show root level content return ( <> {tree.children.map((item, index) => ( - + ))} ); } -// Function to get platform icon and color (now matches the open sidebar colors) -function getPlatformIcon(platform: string): { icon: string, color: string } { - const platformInfo: Record = { - 'next': { icon: 'N', color: 'rgb(59, 130, 246)' }, // Blue - matches homepage/sidebar - 'react': { icon: 'R', color: 'rgb(16, 185, 129)' }, // Green - matches homepage/sidebar - 'js': { icon: 'J', color: 'rgb(245, 158, 11)' }, // Yellow - matches homepage/sidebar - 'python': { icon: 'P', color: 'rgb(168, 85, 247)' } // Purple - matches homepage/sidebar - }; - return platform in platformInfo ? platformInfo[platform] : { icon: '?', color: 'rgb(100, 116, 139)' }; -} - -// Get platform display name -function getPlatformDisplayName(platform: string): string { - const platformNames: Record = { - 'next': 'Next.js', - 'react': 'React', - 'js': 'JavaScript', - 'python': 'Python' - }; - return platform in platformNames ? platformNames[platform] : platform; -} - -// Collapsed Platform Switcher Component -function CollapsedPlatformSwitcher({ currentPlatform }: { currentPlatform?: string }) { - const [isOpen, setIsOpen] = useState(false); - const [hoveredPlatform, setHoveredPlatform] = useState(null); - const dropdownRef = useRef(null); - const buttonRef = useRef(null); - const [buttonRect, setButtonRect] = useState(null); - const pathname = usePathname(); - const router = useRouter(); - const { setPreferredPlatform } = usePlatformPreference(); - - // Update button position when opened - useEffect(() => { - if (isOpen && buttonRef.current) { - setButtonRect(buttonRef.current.getBoundingClientRect()); - } - }, [isOpen]); - - // Click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && - buttonRef.current && !buttonRef.current.contains(event.target as Node)) { - setIsOpen(false); - setHoveredPlatform(null); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - if (!currentPlatform) return null; - - const { icon, color } = getPlatformIcon(currentPlatform); - const platforms = ['next', 'react', 'js', 'python']; - - const handlePlatformChange = (platform: string) => { - setPreferredPlatform(platform as Platform); - setIsOpen(false); - setHoveredPlatform(null); - - // Use smart navigation like the open sidebar - const smartUrl = getSmartRedirectUrl(pathname, platform as Platform); - router.push(smartUrl); - }; - - const dropdownContent = isOpen && buttonRect ? ( -
- {platforms.map((platform) => { - const isSelected = currentPlatform === platform; - const isHovered = hoveredPlatform === platform; - const isHighlighted = isSelected || isHovered; - const { icon: platformIcon, color: platformColor } = getPlatformIcon(platform); - - return ( - - ); - })} -
- ) : null; - - return ( -
- - - {/* Render dropdown as portal to avoid clipping */} - {typeof window !== 'undefined' && dropdownContent && createPortal(dropdownContent, document.body)} - - {/* Tooltip - only show when not open */} - {!isOpen && ( -
- {getPlatformDisplayName(currentPlatform)} Docs -
- )} -
- ); -} - // Enhanced page dot tooltips function CollapsedPageDot({ href, title, - currentPlatform }: { href: string, title: string, - currentPlatform?: string, }) { const [isHovered, setIsHovered] = useState(false); const [dotRect, setDotRect] = useState(null); @@ -554,8 +341,7 @@ function CollapsedPageDot({ // Determine if this page is currently active based on current pathname const isCurrentlyActive = pathname === href; - // Get platform color, fallback to muted if no platform - const platformColor = currentPlatform ? getPlatformIcon(currentPlatform).color : 'rgb(100, 116, 139)'; + const platformColor = 'rgb(59, 130, 246)'; // Update dot position when hovered useEffect(() => { @@ -565,17 +351,7 @@ function CollapsedPageDot({ }, [isHovered]); const handleClick = () => { - // Use smart navigation with fallback logic for page dots too - try { - router.push(href); - } catch (error) { - // If that fails, use smart platform redirect - const currentPlatform = getCurrentPlatform(href); - if (currentPlatform) { - const fallbackUrl = getSmartPlatformRedirect(href, currentPlatform as Platform); - router.push(fallbackUrl); - } - } + router.push(href); }; // Enhanced tooltip with more context @@ -619,7 +395,7 @@ function CollapsedPageDot({ )} style={{ backgroundColor: isCurrentlyActive ? platformColor : 'rgb(100, 116, 139)', - opacity: isCurrentlyActive ? 1 : 0.6 + opacity: isCurrentlyActive ? 1 : 0.6, }} /> @@ -647,14 +423,12 @@ function CollapsedSectionDot({ href, level = 0, items = [], - currentPlatform, defaultOpen = false }: { title: string, href?: string, level?: number, items?: CollapsedItem[], - currentPlatform?: string, defaultOpen?: boolean, }) { const [isOpen, setIsOpen] = useState(defaultOpen); @@ -667,7 +441,7 @@ function CollapsedSectionDot({ // Determine if this section is currently active based on current pathname const isActive = href ? pathname === href : false; - const platformColor = currentPlatform ? getPlatformIcon(currentPlatform).color : 'rgb(100, 116, 139)'; + const platformColor = 'rgb(59, 130, 246)'; useEffect(() => { if (isHovered && dotRef.current) { @@ -676,22 +450,10 @@ function CollapsedSectionDot({ }, [isHovered]); const handleNavigation = () => { - // Expand the section when navigating to it setIsOpen(true); - // Use smart navigation with fallback logic if (href) { - try { - // Try the direct href first - router.push(href); - } catch (error) { - // If that fails, use smart platform redirect - const currentPlatform = getCurrentPlatform(href); - if (currentPlatform) { - const fallbackUrl = getSmartPlatformRedirect(href, currentPlatform as Platform); - router.push(fallbackUrl); - } - } + router.push(href); } }; @@ -814,7 +576,6 @@ function CollapsedSectionDot({ ))} @@ -826,19 +587,17 @@ function CollapsedSectionDot({ function CollapsedSeparatorDot({ title, level = 0, - currentPlatform, sectionItems = [] }: { title: string, level?: number, - currentPlatform?: string, sectionItems?: string[], }) { const [isHovered, setIsHovered] = useState(false); const [dotRect, setDotRect] = useState(null); const dotRef = useRef(null); - const platformColor = currentPlatform ? getPlatformIcon(currentPlatform).color : 'rgb(100, 116, 139)'; + const platformColor = 'rgb(59, 130, 246)'; // Update position when hovered useEffect(() => { @@ -907,11 +666,9 @@ function CollapsedSeparatorDot({ // Main hierarchical item renderer function CollapsedHierarchicalItem({ item, - currentPlatform, nextItems = [] }: { item: CollapsedItem, - currentPlatform?: string, nextItems?: CollapsedItem[], }) { @@ -940,7 +697,6 @@ function CollapsedHierarchicalItem({ ); @@ -953,7 +709,6 @@ function CollapsedHierarchicalItem({ href={item.href} level={item.level} items={item.children} - currentPlatform={currentPlatform} defaultOpen={item.defaultOpen} /> ); @@ -965,7 +720,6 @@ function CollapsedHierarchicalItem({
); @@ -1027,42 +781,15 @@ function convertToHierarchicalStructure(nodes: PageTree.Node[], currentPath: str // Updated collapsed sidebar renderer - uses platform content function renderCollapsedSidebarContent(tree: PageTree.Root, pathname: string) { - const currentPlatform = getCurrentPlatform(pathname) || undefined; - - if (!currentPlatform) return null; - - let hierarchicalItems: CollapsedItem[] = []; - - // For platform-specific content, use the platform folder - const platformContent = findPlatformContent(tree, currentPlatform); - if (platformContent.length > 0) { - hierarchicalItems = convertToHierarchicalStructure(platformContent, pathname); - } else { - // Fallback to root tree if no platform-specific content found - hierarchicalItems = convertToHierarchicalStructure(tree.children, pathname); - } + const hierarchicalItems = convertToHierarchicalStructure(tree.children, pathname); return (
- {/* Platform switcher at the top */} -
- -
- - {/* Separator */} - {currentPlatform && ( -
-
-
- )} - - {/* Hierarchical navigation */}
{hierarchicalItems.map((item, index) => ( ))} @@ -1097,10 +824,6 @@ export function DocsLayout({ }: DocsLayoutProps): ReactNode { const [searchOpen, setSearchOpen] = useState(false); - const tabs = useMemo( - () => getSidebarTabsFromOptions(sidebar.tabs, props.tree) ?? [], - [sidebar.tabs, props.tree], - ); const links = getLinks(props.links ?? [], props.githubUrl); const variables = cn( @@ -1169,12 +892,7 @@ export function DocsLayout({ {nav.children} } - banner={ - <> - {tabs.length > 0 ? : null} - {sidebar.banner} - - } + banner={sidebar.banner} footer={ <> void, - /** Sidebar content to show in mobile navigation */ - sidebarContent?: ReactNode, -} - -/** - * PLATFORM-AWARE HEADER WRAPPER - * - * Client component that wraps SharedHeader with platform persistence logic. - * This allows the header to remember the user's last visited platform - * when navigating between docs and API sections. - */ -export function PlatformAwareHeader({ - showSearch = false, - className, - onMobileMenuClick, - sidebarContent -}: PlatformAwareHeaderProps) { - const platform = usePlatformPersistence(); - const navLinks = generateNavLinks(platform); - - return ( - - ); -} diff --git a/docs/src/components/layouts/shared/section-utils.ts b/docs/src/components/layouts/shared/section-utils.ts index 1d12aafe0a..1043882594 100644 --- a/docs/src/components/layouts/shared/section-utils.ts +++ b/docs/src/components/layouts/shared/section-utils.ts @@ -3,12 +3,14 @@ */ // Helper functions to detect sections +const PLATFORM_PREFIX = '(?:[a-z-]+/)?'; + export function isInSdkSection(pathname: string): boolean { - return /\/docs\/[a-z]+\/sdk(?:\/.*)?$/.test(pathname); + return new RegExp(`^/docs/${PLATFORM_PREFIX}sdk(?:/.*)?$`).test(pathname); } export function isInComponentsSection(pathname: string): boolean { - return /\/docs\/[a-z]+\/components(?:\/.*)?$/.test(pathname); + return new RegExp(`^/docs/${PLATFORM_PREFIX}components(?:/.*)?$`).test(pathname); } export function isInApiSection(pathname: string): boolean { @@ -16,16 +18,5 @@ export function isInApiSection(pathname: string): boolean { } export function isInCustomizationSection(pathname: string): boolean { - return /\/docs\/[a-z]+\/customization(?:\/.*)?$/.test(pathname); -} - -// Platform display name mapping -export function getPlatformDisplayName(platform: string): string { - const platformNames: Record = { - 'next': 'Next.js', - 'react': 'React', - 'js': 'JavaScript', - 'python': 'Python' - }; - return platformNames[platform] || platform; + return new RegExp(`^/docs/${PLATFORM_PREFIX}customization(?:/.*)?$`).test(pathname); } diff --git a/docs/src/components/platform-redirect.tsx b/docs/src/components/platform-redirect.tsx deleted file mode 100644 index abf90ba3ed..0000000000 --- a/docs/src/components/platform-redirect.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; -import { usePlatformPreference } from '../hooks/use-platform-preference'; -import { getSmartPlatformRedirect } from '../lib/platform-navigation'; -import { getCurrentPlatform } from '../lib/platform-utils'; - -type PlatformRedirectProps = { - /** - * The current pathname to check if platform is missing - */ - pathname: string, - /** - * Default path to redirect to after adding platform (e.g., 'overview', 'getting-started') - */ - defaultPath?: string, - /** - * Whether to show loading state during redirect - */ - showLoading?: boolean, -}; - -/** - * Component that redirects users to their preferred platform when they visit - * docs without a specific platform in the URL - */ -export function PlatformRedirect({ - pathname, - defaultPath = 'overview', - showLoading = true -}: PlatformRedirectProps) { - const router = useRouter(); - const { preferredPlatform, isLoaded } = usePlatformPreference(); - - useEffect(() => { - // Only redirect if preferences are loaded and we're not already on a platform-specific page - if (isLoaded) { - const currentPlatform = getCurrentPlatform(pathname); - - // If we're on /docs or /docs/ without a platform, redirect to preferred platform - if (pathname === '/docs' || pathname === '/docs/') { - const redirectUrl = `/docs/${preferredPlatform}/${defaultPath}`; - router.replace(redirectUrl); - return; - } - - // If we're on a docs page but no platform detected, use smart navigation - if (pathname.startsWith('/docs/') && !currentPlatform) { - const redirectUrl = getSmartPlatformRedirect(pathname, preferredPlatform); - router.replace(redirectUrl); - return; - } - } - }, [isLoaded, pathname, preferredPlatform, defaultPath, router]); - - // Show loading state while redirecting - if (showLoading && !isLoaded) { - return ( -
-
-
-

Loading...

-
-
- ); - } - - return null; -} - -/** - * Hook version of platform redirect for use in components - */ -export function usePlatformRedirect(pathname: string, defaultPath = 'overview') { - const router = useRouter(); - const { preferredPlatform, isLoaded } = usePlatformPreference(); - - useEffect(() => { - if (isLoaded) { - const currentPlatform = getCurrentPlatform(pathname); - - if (pathname === '/docs' || pathname === '/docs/') { - const redirectUrl = `/docs/${preferredPlatform}/${defaultPath}`; - router.replace(redirectUrl); - return; - } - - if (pathname.startsWith('/docs/') && !currentPlatform) { - const redirectUrl = getSmartPlatformRedirect(pathname, preferredPlatform); - router.replace(redirectUrl); - return; - } - } - }, [isLoaded, pathname, preferredPlatform, defaultPath, router]); - - return { isLoaded }; -} diff --git a/docs/src/components/sdk/overview.tsx b/docs/src/components/sdk/overview.tsx index 3e84850fa6..57b153b98d 100644 --- a/docs/src/components/sdk/overview.tsx +++ b/docs/src/components/sdk/overview.tsx @@ -1,10 +1,8 @@ 'use client'; import Link from 'fumadocs-core/link'; -import { usePathname } from 'next/navigation'; import type { ReactNode } from 'react'; import { cn } from '../../lib/cn'; -import { DEFAULT_PLATFORM, getCurrentPlatform, getPlatformUrl } from '../../lib/platform-utils'; import { Box, Code, Zap } from '../icons'; type SDKItem = { @@ -57,19 +55,14 @@ function getColorForType(type: string): string { } export function SDKOverview({ sections }: SDKOverviewProps) { - const pathname = usePathname(); - const currentPlatform = getCurrentPlatform(pathname); - // Function to build proper absolute URLs for SDK links const buildSDKUrl = (href: string): string => { // If href already starts with /, it's already absolute if (href.startsWith('/')) return href; - // Use the current platform or fallback to default platform - const platform = currentPlatform || DEFAULT_PLATFORM; - - // Build the absolute URL using getPlatformUrl utility - return getPlatformUrl(platform, `sdk/${href}`); + // Build the absolute URL relative to the shared SDK section + const normalized = href.replace(/^\/+/, ''); + return `/docs/sdk/${normalized}`; }; return ( diff --git a/docs/src/hooks/use-platform-persistence.ts b/docs/src/hooks/use-platform-persistence.ts deleted file mode 100644 index 7c693e8fd1..0000000000 --- a/docs/src/hooks/use-platform-persistence.ts +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; -import { usePathname } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { DEFAULT_PLATFORM, getCurrentPlatform, type Platform } from '../lib/platform-utils'; - -const STORAGE_KEY = 'stack-docs-platform'; - -/** - * Hook that manages platform persistence across docs and API navigation. - * - * When user is on docs pages, it detects and stores the current platform. - * When user is on API pages, it returns the stored platform or default. - * This allows seamless navigation back to the correct platform. - */ -export function usePlatformPersistence(): Platform { - const pathname = usePathname(); - const [storedPlatform, setStoredPlatform] = useState(DEFAULT_PLATFORM); - const [isClient, setIsClient] = useState(false); - - // Handle client-side hydration - useEffect(() => { - setIsClient(true); - - // Load stored platform from localStorage on client mount - const stored = localStorage.getItem(STORAGE_KEY); - if (stored && ['next', 'react', 'js', 'python'].includes(stored)) { - setStoredPlatform(stored as Platform); - } - }, []); - - useEffect(() => { - if (!isClient) return; - - // Get current platform from URL (if on docs pages) - const currentPlatform = getCurrentPlatform(pathname); - - if (currentPlatform) { - // On docs pages - store the current platform - localStorage.setItem(STORAGE_KEY, currentPlatform); - setStoredPlatform(currentPlatform as Platform); - } - // Note: We don't override stored platform when on non-docs pages - // This preserves the last visited platform for navigation - }, [pathname, isClient]); - - return storedPlatform; -} diff --git a/docs/src/hooks/use-platform-preference.ts b/docs/src/hooks/use-platform-preference.ts deleted file mode 100644 index 2819ab114d..0000000000 --- a/docs/src/hooks/use-platform-preference.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useEffect, useState } from 'react'; -import { DEFAULT_PLATFORM, type Platform, PLATFORMS } from '../lib/platform-utils'; - -const PLATFORM_PREFERENCE_KEY = 'stack-auth-preferred-platform'; - -/** - * Type guard to check if a value is a valid Platform - */ -function isValidPlatform(value: unknown): value is Platform { - return typeof value === 'string' && PLATFORMS.includes(value as Platform); -} - -/** - * Hook to manage platform preference persistence in localStorage - * @returns {Object} { preferredPlatform, setPreferredPlatform, isLoaded } - */ -export function usePlatformPreference() { - const [preferredPlatform, setPreferredPlatformState] = useState(DEFAULT_PLATFORM); - const [isLoaded, setIsLoaded] = useState(false); - - // Load preference from localStorage on mount - useEffect(() => { - try { - const stored = localStorage.getItem(PLATFORM_PREFERENCE_KEY); - if (stored && isValidPlatform(stored)) { - setPreferredPlatformState(stored); - } - // If no valid stored preference, keep the DEFAULT_PLATFORM that was set in useState - } catch (error) { - console.warn('Failed to load platform preference from localStorage:', error); - // Keep the DEFAULT_PLATFORM that was set in useState - } finally { - setIsLoaded(true); - } - }, []); - - // Function to update preference in both state and localStorage - const setPreferredPlatform = (platform: Platform) => { - if (!isValidPlatform(platform)) { - console.warn('Invalid platform provided:', platform); - return; - } - - setPreferredPlatformState(platform); - try { - localStorage.setItem(PLATFORM_PREFERENCE_KEY, platform); - } catch (error) { - console.warn('Failed to save platform preference to localStorage:', error); - } - }; - - return { - preferredPlatform, - setPreferredPlatform, - isLoaded - }; -} - -/** - * Get the stored platform preference without React hooks (for use in server components or utilities) - * @returns {Platform} The preferred platform or default platform - */ -export function getStoredPlatformPreference(): Platform { - if (typeof window === 'undefined') { - return DEFAULT_PLATFORM; - } - - try { - const stored = localStorage.getItem(PLATFORM_PREFERENCE_KEY); - if (stored && isValidPlatform(stored)) { - return stored; - } - } catch (error) { - console.warn('Failed to get stored platform preference:', error); - } - - return DEFAULT_PLATFORM; -} diff --git a/docs/src/lib/navigation-utils.ts b/docs/src/lib/navigation-utils.ts index 176b24d2b3..8d80202691 100644 --- a/docs/src/lib/navigation-utils.ts +++ b/docs/src/lib/navigation-utils.ts @@ -1,6 +1,4 @@ import { Book, Code, Layers, Zap, type LucideIcon } from 'lucide-react'; -import { getSmartPlatformRedirect } from './platform-navigation'; -import type { Platform } from './platform-utils'; export type NavLink = { href: string, @@ -8,84 +6,38 @@ export type NavLink = { icon: LucideIcon, } -/** - * Determines if a platform supports React components - */ -export function platformSupportsComponents(platform: Platform): boolean { - return ['next', 'react'].includes(platform); -} - -/** - * Determines if a platform supports SDK - */ -export function platformSupportsSDK(platform: Platform): boolean { - return ['next', 'react', 'js'].includes(platform); -} - -/** - * Determines the appropriate redirect URL when switching platforms. - * Uses docs-platform.yml to find the best alternative page or falls back to overview. - */ -export function getSmartRedirectUrl(currentPath: string, newPlatform: Platform): string { - return getSmartPlatformRedirect(currentPath, newPlatform); -} +const DOCS_GUIDES_PATH = '/docs/overview'; +const DOCS_SDK_PATH = '/docs/sdk'; +const DOCS_COMPONENTS_PATH = '/docs/components'; +const API_OVERVIEW_PATH = '/api/overview'; -/** - * Gets the current platform's URL for the current path - */ -export function getCurrentPlatformUrl(currentPath: string, platform: Platform): string { - // Check for components section specifically: /docs/{platform}/components/... - const componentsMatch = currentPath.match(/^\/docs\/[a-z]+\/components(?:\/.*)?$/); - if (componentsMatch) { - const componentPath = currentPath.replace(/^\/docs\/[a-z]+\/components/, ''); - return `/docs/${platform}/components${componentPath}`; - } - - // Check for SDK section specifically: /docs/{platform}/sdk/... - const sdkMatch = currentPath.match(/^\/docs\/[a-z]+\/sdk(?:\/.*)?$/); - if (sdkMatch) { - const sdkPath = currentPath.replace(/^\/docs\/[a-z]+\/sdk/, ''); - return `/docs/${platform}/sdk${sdkPath}`; - } - - // For general docs within a platform: /docs/{platform}/* - const generalMatch = currentPath.match(/^\/docs\/[a-z]+(\/.*)$/); - if (generalMatch) { - const pathAfterPlatform = generalMatch[1]; - return `/docs/${platform}${pathAfterPlatform}`; - } - - return `/docs/${platform}/overview`; -} - -/** - * Generates platform-aware navigation links for the shared header. - * Ensures navigation links point to the correct platform-specific routes. - * Conditionally includes Components and SDK based on platform support. - */ -export function generateNavLinks(platform: Platform): NavLink[] { - const baseLinks = [ +export function generateNavLinks(): NavLink[] { + return [ + { + href: DOCS_GUIDES_PATH, + label: 'Guides', + icon: Book, + }, + { + href: DOCS_SDK_PATH, + label: 'SDK', + icon: Code, + }, { - href: `/docs/${platform}/overview`, - label: "Guides", - icon: Book + href: DOCS_COMPONENTS_PATH, + label: 'Components', + icon: Layers, }, - ...platformSupportsSDK(platform) ? [{ - href: `/docs/${platform}/sdk`, - label: "SDK", - icon: Code - }] : [], - ...platformSupportsComponents(platform) ? [{ - href: `/docs/${platform}/components`, - label: "Components", - icon: Layers - }] : [], { - href: "/api/overview", - label: "API Reference", - icon: Zap - } + href: API_OVERVIEW_PATH, + label: 'API Reference', + icon: Zap, + }, ]; - - return baseLinks; } + +export const DOCS_NAV_PATHS = { + guides: DOCS_GUIDES_PATH, + sdk: DOCS_SDK_PATH, + components: DOCS_COMPONENTS_PATH, +}; diff --git a/docs/src/lib/platform-utils.ts b/docs/src/lib/platform-utils.ts deleted file mode 100644 index 031e5e9f0e..0000000000 --- a/docs/src/lib/platform-utils.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Extract the current platform from the URL path - * @param pathname - The current pathname (e.g., "/docs/next/overview") - * @returns The platform name (e.g., "next") or null if not found - */ -export function getCurrentPlatform(pathname: string): string | null { - const match = pathname.match(/^\/docs\/([a-z]+)/); - if (match) { - const platform = match[1]; - // Only return if it's a valid platform, not other sections like 'api' - if (PLATFORMS.includes(platform as Platform)) { - return platform; - } - } - return null; -} - -/** - * Generate a platform-specific URL - * @param platform - The platform name (e.g., "next", "react", "js", "python") - * @param path - The relative path (e.g., "overview", "components/overview") - * @returns The full platform-specific URL - */ -export function getPlatformUrl(platform: string, path: string): string { - return `/docs/${platform}/${path}`; -} - -/** - * Available platforms - */ -export const PLATFORMS = ['next', 'react', 'js', 'python'] as const; -export type Platform = typeof PLATFORMS[number]; - -/** - * Default platform to redirect to when no platform is specified - */ -export const DEFAULT_PLATFORM: Platform = 'next'; diff --git a/package.json b/package.json index ba5993d227..1efeb9a0da 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:backend": "pnpm pre && turbo run build --filter=@stackframe/stack-backend...", "build:dashboard": "pnpm pre && turbo run build --filter=@stackframe/stack-dashboard...", "build:demo": "pnpm pre && turbo run build --filter=demo-app...", - "build:docs": "pnpm run build:packages-only && pnpm run build:backend && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-docs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", + "build:docs": "pnpm run build:packages-only && pnpm run build:backend && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", "build:packages": "pnpm pre && turbo run build --filter=./packages/*", "build:packages-only": "turbo run build --filter=./packages/*", "claude-code": "pnpm pre && npx -y @anthropic-ai/claude-code@latest", @@ -41,10 +41,10 @@ "db:init": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:init", "db:migrate": "pnpm pre && pnpm run --filter=@stackframe/stack-backend db:migrate", "fern": "pnpm pre && pnpm run --filter=@stackframe/docs fern", - "dev:full": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-docs:watch\" \"turbo run dev --concurrency 99999\"", - "dev": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-docs:watch\" \"pnpm run generate-openapi-docs:watch\" \"turbo run dev --concurrency 99999 --filter=./apps/* --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo \"", - "dev:basic": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-docs:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/stack-backend --filter=@stackframe/stack-dashboard --filter=@stackframe/mock-oauth-server\"", - "dev:docs": "pnpm pre && concurrently -k \"pnpm run generate-docs:watch\" \"pnpm run generate-openapi-docs:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/stack-docs\"", + "dev:full": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"turbo run dev --concurrency 99999\"", + "dev": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"pnpm run generate-openapi-docs:watch\" \"turbo run dev --concurrency 99999 --filter=./apps/* --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo \"", + "dev:basic": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/stack-backend --filter=@stackframe/stack-dashboard --filter=@stackframe/mock-oauth-server\"", + "dev:docs": "pnpm pre && concurrently -k \"pnpm run generate-openapi-docs:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/stack-docs\"", "dev:named": "pnpm pre && concurrently -k \"pnpm run dev\" \"node -e \\\"process.title='node (stack-named-dev-server)'; process.stdin.resume();\\\"\"", "kill-dev:named": "(pgrep -f 'stack-named-dev-server' | xargs -r -n1 pkill -P); echo 'Killed named dev server (if found). Sleeping to give some time for it to shut down...' && sleep 10", "start": "pnpm pre && turbo run start --concurrency 99999", @@ -63,8 +63,6 @@ "generate-keys": "pnpm pre && turbo run generate-keys", "generate-sdks": "npx --package=tsx tsx ./scripts/generate-sdks.ts", "generate-sdks:watch": "chokidar --silent -c 'pnpm run generate-sdks' './packages/template' --ignore './packages/template/package.json' --ignore '**/node_modules/**' --ignore '**/dist/**' --ignore '**/.turbo/**' --throttle 2000", - "generate-docs": "pnpm run build:packages-only && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-docs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs", - "generate-docs:watch": "chokidar --silent -c 'pnpm run generate-docs' './docs/templates' --throttle 2000", "generate-openapi-docs:watch": "chokidar --silent -c 'pnpm run --filter=@stackframe/stack-docs generate-openapi-docs' './docs/public/openapi/{admin,client,server,webhooks}.json' --throttle 2000" }, "devDependencies": { From 06053dbfd717d9e06fa5d9dbccb3bebcb362a5d5 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 29 Sep 2025 23:15:44 -0500 Subject: [PATCH 02/55] fix sidebar rendering --- .../layouts/docs-header-wrapper.tsx | 7 +- .../components/layouts/docs-layout-router.tsx | 8 +- docs/src/lib/docs-tree.ts | 171 ++++++++++++++++++ 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 docs/src/lib/docs-tree.ts diff --git a/docs/src/components/layouts/docs-header-wrapper.tsx b/docs/src/components/layouts/docs-header-wrapper.tsx index 30e5329b2e..d14aa35368 100644 --- a/docs/src/components/layouts/docs-header-wrapper.tsx +++ b/docs/src/components/layouts/docs-header-wrapper.tsx @@ -1,4 +1,5 @@ 'use client'; +import { filterTreeForSection, resolveDocsSection } from '@/lib/docs-tree'; import type { PageTree } from 'fumadocs-core/server'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { usePathname } from 'next/navigation'; @@ -250,6 +251,8 @@ function GeneralDocsSidebarContent({ pageTree }: { pageTree?: PageTree.Root }) { export function DocsHeaderWrapper({ showSearch = true, pageTree, className, apiPages }: DocsHeaderWrapperProps) { const pathname = usePathname(); const navLinks = useMemo(() => generateNavLinks(), []); + const docsSection = resolveDocsSection(pathname); + const sectionTree = useMemo(() => (pageTree ? filterTreeForSection(pageTree, docsSection) : undefined), [pageTree, docsSection]); // Determine current sidebar content based on route const sidebarContent = useMemo(() => { @@ -259,11 +262,11 @@ export function DocsHeaderWrapper({ showSearch = true, pageTree, className, apiP // For all docs pages, use the page tree if (pathname.startsWith('/docs') && !isInApiSection(pathname)) { - return ; + return ; } return null; - }, [pathname, pageTree, apiPages]); + }, [pathname, apiPages, sectionTree]); return ( filterTreeForSection(props.tree, section), [props.tree, section]); // For API docs, use minimal layout without platform tabs if (isInApiSection(pathname)) { @@ -77,7 +81,7 @@ export function DynamicDocsLayout({ children, ...props }: DynamicDocsLayoutProps filterNode(child, section)) + .filter((node): node is PageTree.Node => node !== null), + ), + ); + + return { + ...tree, + children: filteredChildren, + }; +} + +function filterNode(node: PageTree.Node, section: DocsSection): PageTree.Node | null { + if (node.type === 'separator') { + return node; + } + + if (node.type === 'page') { + return matchesSection(node.url, section) ? node : null; + } + + const { + index: originalIndex, + children: originalChildren, + ...rest + } = node; + + const filteredChildren = pruneSeparators( + originalChildren + .map(child => filterNode(child, section)) + .filter((child): child is PageTree.Node => child !== null), + ); + + const filteredIndex = originalIndex && matchesSection(originalIndex.url, section) + ? originalIndex + : undefined; + + if (filteredChildren.length === 0 && !filteredIndex) { + return null; + } + + const folder: PageTree.Folder = { + ...rest, + type: 'folder', + children: filteredChildren, + } as PageTree.Folder; + + if (filteredIndex) { + folder.index = filteredIndex; + } + + return folder; +} + +function matchesSection(url: string, section: DocsSection): boolean { + const cleaned = normalizeUrl(url); + + if (!cleaned.startsWith(DOCS_ROOT)) { + return false; + } + + if (section === 'sdk') { + return cleaned === `${DOCS_ROOT}/sdk` || cleaned.startsWith(`${DOCS_ROOT}/sdk/`); + } + + if (section === 'components') { + return cleaned === `${DOCS_ROOT}/components` || cleaned.startsWith(`${DOCS_ROOT}/components/`); + } + + if (cleaned.startsWith(`${DOCS_ROOT}/sdk`)) { + return false; + } + + if (cleaned.startsWith(`${DOCS_ROOT}/components`)) { + return false; + } + + return cleaned === DOCS_ROOT || cleaned.startsWith(`${DOCS_ROOT}/`); +} + +function normalizeUrl(url: string): string { + const withoutFragment = url.split('#')[0]; + return withoutFragment.replace(/\/$/, ''); +} + +function pruneSeparators(nodes: PageTree.Node[]): PageTree.Node[] { + if (nodes.length === 0) { + return nodes; + } + + let start = 0; + while (start < nodes.length && nodes[start]?.type === 'separator') { + start += 1; + } + + let end = nodes.length - 1; + while (end >= start && nodes[end]?.type === 'separator') { + end -= 1; + } + + if (start > end) { + return []; + } + + const result: PageTree.Node[] = []; + for (let i = start; i <= end; i += 1) { + const current = nodes[i]; + + if (current.type === 'separator' && result[result.length - 1]?.type === 'separator') { + continue; + } + + result.push(current); + } + + return result; +} + +function flattenRootChildren(nodes: PageTree.Node[]): PageTree.Node[] { + if (nodes.length !== 1) { + return nodes; + } + + const soleNode = nodes[0] as PageTree.Node; + if (soleNode.type !== 'folder') { + return nodes; + } + + const flattened: PageTree.Node[] = []; + const seenUrls = new Set(); + + if (soleNode.index) { + const normalized = normalizeUrl(soleNode.index.url); + seenUrls.add(normalized); + flattened.push(soleNode.index); + } + + soleNode.children.forEach(child => { + if (child.type === 'page') { + const normalized = normalizeUrl(child.url); + if (seenUrls.has(normalized)) { + return; + } + seenUrls.add(normalized); + } + flattened.push(child); + }); + + return pruneSeparators(flattened); +} From 47ccd2bd4a44e3fc8794a3dc38e14883ba14e1da Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 30 Sep 2025 11:08:21 -0500 Subject: [PATCH 03/55] init static docs --- docs/.gitignore | 3 +- .../docs/(guides)/concepts/api-keys.mdx | 245 +++ .../concepts/auth-providers/apple.mdx | 72 + .../concepts/auth-providers/bitbucket.mdx | 39 + .../concepts/auth-providers/discord.mdx | 45 + .../concepts/auth-providers/facebook.mdx | 43 + .../concepts/auth-providers/github.mdx | 38 + .../concepts/auth-providers/gitlab.mdx | 38 + .../concepts/auth-providers/google.mdx | 41 + .../concepts/auth-providers/index.mdx | 155 ++ .../concepts/auth-providers/linkedin.mdx | 40 + .../concepts/auth-providers/meta.json | 20 + .../concepts/auth-providers/microsoft.mdx | 43 + .../concepts/auth-providers/passkey.mdx | 55 + .../concepts/auth-providers/spotify.mdx | 41 + .../concepts/auth-providers/twitch.mdx | 36 + .../auth-providers/two-factor-auth.mdx | 59 + .../concepts/auth-providers/x-twitter.mdx | 43 + .../(guides)/concepts/backend-integration.mdx | 135 ++ .../(guides)/concepts/custom-user-data.mdx | 58 + .../content/docs/(guides)/concepts/emails.mdx | 197 ++ docs/content/docs/(guides)/concepts/jwt.mdx | 224 +++ docs/content/docs/(guides)/concepts/oauth.mdx | 118 ++ .../docs/(guides)/concepts/orgs-and-teams.mdx | 246 +++ .../docs/(guides)/concepts/permissions.mdx | 264 +++ .../docs/(guides)/concepts/stack-app.mdx | 50 + .../docs/(guides)/concepts/team-selection.mdx | 118 ++ .../(guides)/concepts/user-onboarding.mdx | 131 ++ .../docs/(guides)/concepts/webhooks.mdx | 69 + .../(guides)/customization/custom-pages.mdx | 82 + .../(guides)/customization/custom-styles.mdx | 53 + .../docs/(guides)/customization/dark-mode.mdx | 61 + .../customization/internationalization.mdx | 37 + .../page-examples/forgot-password.mdx | 97 + .../customization/page-examples/index.mdx | 15 + .../customization/page-examples/meta.json | 10 + .../page-examples/password-reset.mdx | 106 ++ .../customization/page-examples/sign-in.mdx | 141 ++ .../customization/page-examples/sign-up.mdx | 79 + docs/content/docs/(guides)/faq.mdx | 40 + .../(guides)/getting-started/components.mdx | 62 + .../getting-started/example-pages.mdx | 455 +++++ .../(guides)/getting-started/production.mdx | 148 ++ .../docs/(guides)/getting-started/setup.mdx | 595 +++++++ .../docs/(guides)/getting-started/users.mdx | 253 +++ docs/content/docs/(guides)/meta.json | 41 + .../(guides)/others/cli-authentication.mdx | 63 + docs/content/docs/(guides)/others/convex.mdx | 101 ++ .../docs/(guides)/others/self-host.mdx | 159 ++ .../content/docs/(guides)/others/supabase.mdx | 164 ++ docs/content/docs/(guides)/overview.mdx | 112 ++ .../docs/(guides)/rest-api/overview.mdx | 98 + .../docs/components/account-settings.mdx | 71 + .../docs/components/credential-sign-in.mdx | 32 + .../docs/components/credential-sign-up.mdx | 42 + .../docs/components/forgot-password.mdx | 29 + docs/content/docs/components/index.mdx | 150 ++ .../docs/components/magic-link-sign-in.mdx | 24 + docs/content/docs/components/meta.json | 27 + .../docs/components/oauth-button-group.mdx | 41 + docs/content/docs/components/oauth-button.mdx | 49 + .../docs/components/password-reset.mdx | 39 + .../components/selected-team-switcher.mdx | 56 + docs/content/docs/components/sign-in.mdx | 65 + docs/content/docs/components/sign-up.mdx | 71 + .../content/docs/components/stack-handler.mdx | 56 + .../docs/components/stack-provider.mdx | 58 + docs/content/docs/components/stack-theme.mdx | 47 + docs/content/docs/components/user-button.mdx | 79 + docs/content/docs/sdk/hooks/use-stack-app.mdx | 16 + docs/content/docs/sdk/hooks/use-user.mdx | 7 + docs/content/docs/sdk/index.mdx | 57 + docs/content/docs/sdk/meta.json | 25 + docs/content/docs/sdk/objects/stack-app.mdx | 987 +++++++++++ docs/content/docs/sdk/overview-new.mdx | 51 + docs/content/docs/sdk/types/api-key.mdx | 437 +++++ .../docs/sdk/types/connected-account.mdx | 5 + .../docs/sdk/types/contact-channel.mdx | 308 ++++ docs/content/docs/sdk/types/email.mdx | 201 +++ docs/content/docs/sdk/types/project.mdx | 90 + .../docs/sdk/types/team-permission.mdx | 30 + docs/content/docs/sdk/types/team-profile.mdx | 65 + docs/content/docs/sdk/types/team-user.mdx | 90 + docs/content/docs/sdk/types/team.mdx | 690 ++++++++ docs/content/docs/sdk/types/user.mdx | 1577 +++++++++++++++++ 85 files changed, 11208 insertions(+), 2 deletions(-) create mode 100644 docs/content/docs/(guides)/concepts/api-keys.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/apple.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/bitbucket.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/discord.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/facebook.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/github.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/gitlab.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/google.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/index.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/linkedin.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/meta.json create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/microsoft.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/passkey.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/spotify.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/twitch.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/two-factor-auth.mdx create mode 100644 docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx create mode 100644 docs/content/docs/(guides)/concepts/backend-integration.mdx create mode 100644 docs/content/docs/(guides)/concepts/custom-user-data.mdx create mode 100644 docs/content/docs/(guides)/concepts/emails.mdx create mode 100644 docs/content/docs/(guides)/concepts/jwt.mdx create mode 100644 docs/content/docs/(guides)/concepts/oauth.mdx create mode 100644 docs/content/docs/(guides)/concepts/orgs-and-teams.mdx create mode 100644 docs/content/docs/(guides)/concepts/permissions.mdx create mode 100644 docs/content/docs/(guides)/concepts/stack-app.mdx create mode 100644 docs/content/docs/(guides)/concepts/team-selection.mdx create mode 100644 docs/content/docs/(guides)/concepts/user-onboarding.mdx create mode 100644 docs/content/docs/(guides)/concepts/webhooks.mdx create mode 100644 docs/content/docs/(guides)/customization/custom-pages.mdx create mode 100644 docs/content/docs/(guides)/customization/custom-styles.mdx create mode 100644 docs/content/docs/(guides)/customization/dark-mode.mdx create mode 100644 docs/content/docs/(guides)/customization/internationalization.mdx create mode 100644 docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx create mode 100644 docs/content/docs/(guides)/customization/page-examples/index.mdx create mode 100644 docs/content/docs/(guides)/customization/page-examples/meta.json create mode 100644 docs/content/docs/(guides)/customization/page-examples/password-reset.mdx create mode 100644 docs/content/docs/(guides)/customization/page-examples/sign-in.mdx create mode 100644 docs/content/docs/(guides)/customization/page-examples/sign-up.mdx create mode 100644 docs/content/docs/(guides)/faq.mdx create mode 100644 docs/content/docs/(guides)/getting-started/components.mdx create mode 100644 docs/content/docs/(guides)/getting-started/example-pages.mdx create mode 100644 docs/content/docs/(guides)/getting-started/production.mdx create mode 100644 docs/content/docs/(guides)/getting-started/setup.mdx create mode 100644 docs/content/docs/(guides)/getting-started/users.mdx create mode 100644 docs/content/docs/(guides)/meta.json create mode 100644 docs/content/docs/(guides)/others/cli-authentication.mdx create mode 100644 docs/content/docs/(guides)/others/convex.mdx create mode 100644 docs/content/docs/(guides)/others/self-host.mdx create mode 100644 docs/content/docs/(guides)/others/supabase.mdx create mode 100644 docs/content/docs/(guides)/overview.mdx create mode 100644 docs/content/docs/(guides)/rest-api/overview.mdx create mode 100644 docs/content/docs/components/account-settings.mdx create mode 100644 docs/content/docs/components/credential-sign-in.mdx create mode 100644 docs/content/docs/components/credential-sign-up.mdx create mode 100644 docs/content/docs/components/forgot-password.mdx create mode 100644 docs/content/docs/components/index.mdx create mode 100644 docs/content/docs/components/magic-link-sign-in.mdx create mode 100644 docs/content/docs/components/meta.json create mode 100644 docs/content/docs/components/oauth-button-group.mdx create mode 100644 docs/content/docs/components/oauth-button.mdx create mode 100644 docs/content/docs/components/password-reset.mdx create mode 100644 docs/content/docs/components/selected-team-switcher.mdx create mode 100644 docs/content/docs/components/sign-in.mdx create mode 100644 docs/content/docs/components/sign-up.mdx create mode 100644 docs/content/docs/components/stack-handler.mdx create mode 100644 docs/content/docs/components/stack-provider.mdx create mode 100644 docs/content/docs/components/stack-theme.mdx create mode 100644 docs/content/docs/components/user-button.mdx create mode 100644 docs/content/docs/sdk/hooks/use-stack-app.mdx create mode 100644 docs/content/docs/sdk/hooks/use-user.mdx create mode 100644 docs/content/docs/sdk/index.mdx create mode 100644 docs/content/docs/sdk/meta.json create mode 100644 docs/content/docs/sdk/objects/stack-app.mdx create mode 100644 docs/content/docs/sdk/overview-new.mdx create mode 100644 docs/content/docs/sdk/types/api-key.mdx create mode 100644 docs/content/docs/sdk/types/connected-account.mdx create mode 100644 docs/content/docs/sdk/types/contact-channel.mdx create mode 100644 docs/content/docs/sdk/types/email.mdx create mode 100644 docs/content/docs/sdk/types/project.mdx create mode 100644 docs/content/docs/sdk/types/team-permission.mdx create mode 100644 docs/content/docs/sdk/types/team-profile.mdx create mode 100644 docs/content/docs/sdk/types/team-user.mdx create mode 100644 docs/content/docs/sdk/types/team.mdx create mode 100644 docs/content/docs/sdk/types/user.mdx diff --git a/docs/.gitignore b/docs/.gitignore index 76d5dc7c4d..e446edebc3 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -27,8 +27,7 @@ yarn-error.log* .vercel next-env.d.ts -# ignore all generated docs content -/content/docs/ +# ignore generated API content /content/api/ /public/openapi/ /openapi/ diff --git a/docs/content/docs/(guides)/concepts/api-keys.mdx b/docs/content/docs/(guides)/concepts/api-keys.mdx new file mode 100644 index 0000000000..146abe7a39 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/api-keys.mdx @@ -0,0 +1,245 @@ +--- +title: API Keys +description: Create and manage API keys for users and teams +--- + +API keys provide a secure way for your users to authenticate with your application's backend. +They enable programmatic access to your API services, allowing developers to associate requests with specific users or teams. +Stack Auth provides prebuilt UI components for the users and teams to manage their own API keys. + +## Overview + +API keys allow your users to access your backend services programmatically. + +>+Server: API request with API key + Server->>+Stack: Validate API key + Stack-->>-Server: Return authenticated User object + Server->>Server: Process request + Server-->>-User: Response with data +`} /> + +Stack Auth provides two types of API keys: + +### User API keys + +User API keys are associated with individual users and allow them to authenticate with your API. + + + + Client + Server + + + ```typescript + const user = await stackApp.getUser(); + + const apiKey = await user.createApiKey({ + description: "My client application", + expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days + isPublic: false, + }); + ``` + + + ```typescript + const user = await stackServerApp.getServerUserById("user-id-here"); + + const apiKey = await user.createApiKey({ + description: "Admin-provisioned API key", + expiresAt: new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)), // 30 days + isPublic: false, + }); + ``` + + + +### Team API keys + +Team API keys are associated with teams and can be used to provide access to team resources over your API. + + + + Client + Server + + + ```typescript + const user = await stackApp.getUser(); + const team = await user.getTeam("team-id-here"); + + const teamApiKey = await team.createApiKey({ + description: "Team integration service", + expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days + isPublic: false, + }); + ``` + + + ```typescript + const team = await stackServerApp.getTeam("team-id-here"); + + const teamApiKey = await team.createApiKey({ + description: "Admin-provisioned team API key", + expiresAt: new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)), // 30 days + isPublic: false, + }); + ``` + + + +## Setting Up API Keys in Stack Auth + +To use API keys in your application, you need to enable them in your project settings. Navigate to the Stack Auth dashboard, select your project, and enable User API Keys and/or Team API Keys in the project settings. + +## Working with API Keys + +### Creating User API Keys + + + + Client + Server + + + ```typescript + const apiKey = await user.createApiKey({ + description: "Development environment key", + expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days from now + isPublic: false, + }); + ``` + + + ```typescript + const userId = "user-id-here"; + const user = await stackServerApp.getServerUserById(userId); + + const apiKey = await user.createApiKey({ + description: "API key created by server", + expiresAt: new Date(Date.now() + (90 * 24 * 60 * 60 * 1000)), // 90 days + isPublic: false, + }); + ``` + + + +### Creating Team API Keys + + + + Client + Server + + + ```typescript + const team = await user.getTeam("team-id-here"); + + const teamApiKey = await team.createApiKey({ + description: "Team service integration", + expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days + isPublic: false, + }); + ``` + + + ```typescript + const team = await stackServerApp.getTeam("team-id-here"); + + const teamApiKey = await team.createApiKey({ + description: "Server-created team API key", + expiresAt: new Date(Date.now() + (60 * 24 * 60 * 60 * 1000)), // 60 days + isPublic: false, + }); + ``` + + + +### Listing API Keys + + + + Client + Server + + + ```typescript + // List user's API keys + const userApiKeys = await user.listApiKeys(); + + // List a team's API keys + const team = await user.getTeam("team-id-here"); + const teamApiKeys = await team.listApiKeys(); + + // Using hooks in React components + const apiKeys = user.useApiKeys(); + const teamApiKeys = team.useApiKeys(); + ``` + + + ```typescript + // List a specific user's API keys + const user = await stackServerApp.getServerUserById("user-id-here"); + const userApiKeys = await user.listApiKeys(); + + // List a team's API keys + const team = await stackServerApp.getTeam("team-id-here"); + const teamApiKeys = await team.listApiKeys(); + ``` + + + +### Revoking API Keys + +API keys can be revoked when they are no longer needed or if they have been compromised. + + + + Client + Server + + + ```typescript + const apiKeys = await user.listApiKeys(); + const apiKeyToRevoke = apiKeys.find(key => key.id === "api-key-id-here"); + + if (apiKeyToRevoke) { + await apiKeyToRevoke.revoke(); + } + ``` + + + ```typescript + const user = await stackServerApp.getServerUserById("user-id-here"); + const apiKeys = await user.listApiKeys(); + + const apiKeyToRevoke = apiKeys.find(key => key.id === "api-key-id-here"); + + if (apiKeyToRevoke) { + await apiKeyToRevoke.revoke(); + } + ``` + + + +### Checking API Key Validity + +You can check if an API key is still valid: + +```typescript +const apiKeys = await user.listApiKeys(); +const apiKey = apiKeys.find(key => key.id === "api-key-id-here"); + +if (apiKey && apiKey.isValid()) { + // API key is valid +} else { + // API key is invalid (expired or revoked) + const reason = apiKey ? apiKey.whyInvalid() : "not found"; + console.log(`API key is invalid: ${reason}`); +} +``` diff --git a/docs/content/docs/(guides)/concepts/auth-providers/apple.mdx b/docs/content/docs/(guides)/concepts/auth-providers/apple.mdx new file mode 100644 index 0000000000..eecb514b97 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/apple.mdx @@ -0,0 +1,72 @@ +--- +title: "Apple" +--- + +This guide explains how to set up Apple as an authentication provider with Stack Auth. Sign in with Apple allows users to sign in to your application using their Apple ID. + + +You will need to create an Apple Developer account, and generate an Apple Services ID, Apple Private Key, Apple Team ID, and Apple Key ID. + + +## Integration Steps + + + + ### Create an Apple App ID and Services ID + + 1. Log in to the [Apple Developer Portal](https://developer.apple.com/). + 2. Navigate to **Certificates, IDs & Profiles**. + 3. In the sidebar, select **Identifiers** and click the "+" button to register a new identifier. + 4. Select **App IDs** and click **Continue**. + 5. Select **App** as the type and click **Continue**. + 6. Give your app a description and a Bundle ID (e.g., com.yourdomain.app). + 7. Scroll down and enable **Sign in with Apple**, then click **Continue**, then **Register**. + 8. In the top-right of the Identifiers page, switch to **Services IDs**. + 9. Click the "+" button to create a new Service ID and click **Continue**. + 10. Give it a description and an identifier (note: this cannot be the same as your App ID's bundle ID). + 11. Click **Continue**, then **Register**. + 12. From the list, select your new Service ID. + 13. Enable **Sign in with Apple** by checking the box. + 14. Click **Configure** next to Sign in with Apple. + 15. Register your domains (add api.stack-auth.com). + 16. Add the return URL: `https://api.stack-auth.com/api/v1/auth/oauth/callback/apple` + 17. Click **Done**, then **Continue**, and then **Save**. + + + ### Create a Private Key + + 1. In the sidebar, select **Keys** and click the "+" button. + 2. Give your key a name and usage description. + 3. Scroll down to enable **Sign in with Apple** and click **Configure**. + 4. Select your Primary App ID that you created earlier and click **Save**. + 5. Click **Continue**, then **Register**. + 6. On the next page, **download your key file (.p8)**. This is critical as you won't be able to download it again. + 7. Note your **Key ID** displayed on this page. + 8. Click **Done**. + 9. Find your **Account ID** at the very top-right of the Apple Developer Portal page. + + + ### Generate Your Client Secret + + 1. Navigate to the [Supabase Apple Secret Generator](https://supabase.com/docs/guides/auth/social-login/auth-apple#generating-a-client_secret) page. + 2. Fill in the required fields: + - **Account ID**: Your Apple Developer account ID found at the top-right of the portal + - **Service ID**: The identifier of your Service ID (found in Identifiers > Service IDs) + - **Key ID**: The ID of the private key you just created + - **Choose File**: Upload the .p8 private key file you downloaded + 3. Click **Generate Secret Key**. + 4. Copy the generated secret immediately - you'll need it for the next step. + + + ### Enable Apple OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Apple** as the provider. + 3. Set the **Client ID** (your Service ID identifier), **Client Secret** (the generated secret from Supabase), and **Team ID** (your Apple Developer Team ID). + + + +### Need More Help? + +- Check the [Sign in with Apple Documentation](https://developer.apple.com/sign-in-with-apple/get-started/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/bitbucket.mdx b/docs/content/docs/(guides)/concepts/auth-providers/bitbucket.mdx new file mode 100644 index 0000000000..ff3624c345 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/bitbucket.mdx @@ -0,0 +1,39 @@ +--- +title: "Bitbucket" +--- + +This guide explains how to set up Bitbucket as an authentication provider with Stack Auth. Bitbucket OAuth allows users to sign in to your application using their Bitbucket account. + + +## Integration Steps + + + + ### Create a Bitbucket OAuth App + + 1. Log in to your [Bitbucket Workspaces account](https://bitbucket.org/account/workspaces/). + 2. Under **Workspaces**, find your workspace and select **Manage**. + 3. In the left sidebar, scroll down and select **OAuth consumers**. + 4. Click **Add consumer**. + 5. Fill out the form with the following details: + - **Name**: Choose a name for your application + - **Description**: Add a brief description of your application + - **Callback URL**: Enter `https://api.stack-auth.com/api/v1/auth/oauth/callback/bitbucket` + - **Permissions**: Under **Account**, select at minimum **Email** and **Read** + 6. Click **Save**. + 7. You'll be redirected to the OAuth consumers page. Select your newly created consumer to view its details. + 8. Note the **Key** (Client ID) and **Secret** values. Save these somewhere secure as you'll need them for the next steps. + + + ### Enable Bitbucket OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Bitbucket** as the provider. + 3. Set the **Client ID** (the Key from your Bitbucket OAuth consumer) and **Client Secret** you obtained from Bitbucket earlier. + + + +### Need More Help? + +- Check the [Bitbucket OAuth documentation](https://developer.atlassian.com/cloud/bitbucket/oauth-2/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/discord.mdx b/docs/content/docs/(guides)/concepts/auth-providers/discord.mdx new file mode 100644 index 0000000000..dd08e48258 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/discord.mdx @@ -0,0 +1,45 @@ +--- +title: "Discord" +--- + +This guide explains how to set up Discord as an authentication provider with Stack Auth. Discord OAuth2 allows users to sign in to your application using their Discord account. + +## Integration Steps + + + + ### Create a Discord Developer App + + 1. Navigate to the [Discord Developer Portal](https://discord.com/developers/applications). + 2. Click the **New Application** button in the top-right corner. + 3. Enter a name for your application and click **Create**. You will be redirected to the General Information page. + 4. Select **OAuth2** in the left sidebar. + 5. Under **Redirects** add `https://api.stack-auth.com/api/v1/auth/oauth/callback/discord` + 6. In the **OAuth2** section, enable the required scopes: 'identify' and 'email' + 7. Click **Save Changes** + 8. Save the **Client ID** and **Client Secret**. You may need to select **Reset Secret** to generate a new one. + + + ### Enable Discord OAuth2 in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Discord** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from the Discord Developer Portal earlier. + + + + +### User Profile Data + +When a user signs in with Discord, Stack Auth will create a user profile with the following data: + +- **User ID**: Discord's unique user ID +- **Username**: The user's Discord username +- **Avatar**: The user's Discord avatar (if available) +- **Email**: The user's email if the 'email' scope is requested + + +### Need More Help? + +- Check the [Discord OAuth2 Documentation](https://discord.com/developers/docs/topics/oauth2) +- Visit our [Discord Support Channel](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/facebook.mdx b/docs/content/docs/(guides)/concepts/auth-providers/facebook.mdx new file mode 100644 index 0000000000..c266fd1969 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/facebook.mdx @@ -0,0 +1,43 @@ +--- +title: "Facebook" +--- + +This guide explains how to set up Facebook as an authentication provider with Stack Auth. Facebook OAuth allows users to sign in to your application using their Facebook account. + + +## Integration Steps + + + + ### Create a Facebook OAuth App + + 1. Navigate to the [Facebook Developers Portal](https://developers.facebook.com/). + 2. In the top-right, select **My Apps** and then **Create App**. + 3. You'll be redirected to the Create an app process. + 4. In the **App details** step, select the app type (typically **Consumer** for authentication), fill out the necessary information, and select **Next**. + 5. In the **Use Cases** step, select **Authenticate and request data from users with Facebook Login** and then select **Next**. + 6. In the **Business** step, select the business portfolio to connect to your app and then select **Next**. + 7. In the **Finalize** step, select **Go to dashboard**. You'll be redirected to the app's Dashboard page. + 8. In the left sidenav, select **Use cases**. + 9. Next to **Authenticate and request data from users with Facebook Login**, select **Customize**. + 10. On the Permissions tab, next to **email**, select **Add** to allow Stack Auth to read your user's primary email address. + 11. In the left sidenav, under **Facebook Login**, select **Settings**. + 12. In the **Client OAuth settings** section, in the **Valid OAuth Redirect URIs** field, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/facebook` + 13. Select **Save changes**. + 14. In the left sidenav, select **App settings** (hover over the settings icon), and then select **Basic**. + 15. Note your **App ID** and **App Secret** for the next steps. + + + + ### Enable Facebook OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Facebook** as the provider. + 3. Set the **App ID** and **App Secret** you obtained from the Facebook Developers Portal earlier. + + + +### Need More Help? + +- Check the [Facebook Login Documentation](https://developers.facebook.com/docs/facebook-login/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/github.mdx b/docs/content/docs/(guides)/concepts/auth-providers/github.mdx new file mode 100644 index 0000000000..8d0e60bbaa --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/github.mdx @@ -0,0 +1,38 @@ +--- +title: "GitHub" +--- + +This guide explains how to set up GitHub as an authentication provider with Stack Auth. GitHub OAuth allows users to sign in to your application using their GitHub account. + + +For Development purposes, Stack Auth uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. +You should replace these before you go into production. + + +## Integration Steps + + + + ### Create a GitHub OAuth App + + 1. Navigate to your [GitHub Developer Settings](https://github.com/settings/developers). + 2. Click the **New OAuth App** button. + 3. Enter a name for your application, homepage URL, and a description. + 4. For **Authorization callback URL**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/github` + 5. Click **Register application** + 6. Save the **Client ID** and click **Generate a new client secret** to create your **Client Secret**. + + + + ### Enable GitHub OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **GitHub** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from GitHub earlier. + + + +### Need More Help? + +- Check the [GitHub OAuth Documentation](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/gitlab.mdx b/docs/content/docs/(guides)/concepts/auth-providers/gitlab.mdx new file mode 100644 index 0000000000..6a75752d30 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/gitlab.mdx @@ -0,0 +1,38 @@ +--- +title: "GitLab" +--- + +This guide explains how to set up GitLab as an authentication provider with Stack Auth. GitLab OAuth allows users to sign in to your application using their GitLab account. + + +## Integration Steps + + + + ### Create a GitLab OAuth App + + 1. Log in to your GitLab account. + 2. In the top-right corner, click on your profile picture and select **Preferences**. + 3. In the left sidebar, select **Applications** > **Add new application**. + 4. Fill out the form with the following details: + - **Name**: Choose a name for your application + - **Redirect URI**: Enter `https://api.stack-auth.com/api/v1/auth/oauth/callback/gitlab` + - **Scopes**: Select at minimum the `profile` and `email` scopes + 5. Click **Save application**. + 6. GitLab will display your **Application ID** and **Secret**. Make note of these values as you'll need them for the next steps. + 7. If you're using a self-hosted GitLab instance, you'll also need the URL of your GitLab instance. + + + ### Enable GitLab OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **GitLab** as the provider. + 3. Set the **Application ID** and **Secret** you obtained from GitLab earlier. + 4. If you're using a self-hosted GitLab instance, you'll also need to provide the URL for your instance. For gitlab.com, you can leave this field blank or use the default value. + + + +### Need More Help? + +- Check the [GitLab OAuth 2.0 documentation](https://docs.gitlab.com/ee/api/oauth2.html) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/google.mdx b/docs/content/docs/(guides)/concepts/auth-providers/google.mdx new file mode 100644 index 0000000000..783bedb4c4 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/google.mdx @@ -0,0 +1,41 @@ +--- +title: "Google" +--- + +This guide explains how to set up Google as an authentication provider with Stack Auth. Google OAuth2 allows users to sign in to your application using their Google account. + + +For Development purposes, Stack Auth uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. +You should replace these before you go into production. + + +## Integration Steps + + + + ### Create a Google OAuth2 App + + 1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/). + 2. Create a new project or select an existing one. + 3. In the sidebar, navigate to **APIs & Services** > **Credentials**. + 4. Click **Create Credentials** and select **OAuth client ID**. + 5. Select **Web application** as the application type. + 6. Enter a name for your OAuth client. + 7. Under **Authorized redirect URIs**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/google` + 8. Click **Create**. + 9. Save the **Client ID** and **Client Secret** that are displayed. + + + + ### Enable Google OAuth2 in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Google** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from Google Cloud Console earlier. + + + +### Need More Help? + +- Check the [Google OAuth2 Documentation](https://developers.google.com/identity/protocols/oauth2) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/index.mdx b/docs/content/docs/(guides)/concepts/auth-providers/index.mdx new file mode 100644 index 0000000000..c64776d9da --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/index.mdx @@ -0,0 +1,155 @@ +--- +title: "Auth Providers Overview" +--- + +Stack Auth supports a wide range of authentication providers to help you add secure authentication to your application. This documentation covers all the supported providers and how to configure them. + +## OAuth Providers + + + +
+ GitHub +
+
+ + +
+ Google +
+
+ + +
+ Facebook +
+
+ + +
+ Microsoft +
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ GitLab +
+
+ + +
+ + + +
+
+ + +
+ BitBucket +
+
+ + +
+ LinkedIn +
+
+ + +
+ +
+
+ + +
+ + + + + + + + + + + + +
+
+
+ +## Other Authentication Methods + + + +
+
+
+ + +
+
+
+
+ +Each provider has its own setup process and configuration requirements. Select a provider from the navigation to learn more about how to configure it with Stack Auth. diff --git a/docs/content/docs/(guides)/concepts/auth-providers/linkedin.mdx b/docs/content/docs/(guides)/concepts/auth-providers/linkedin.mdx new file mode 100644 index 0000000000..b6c224bd10 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/linkedin.mdx @@ -0,0 +1,40 @@ +--- +title: "LinkedIn" +--- + +This guide explains how to set up LinkedIn as an authentication provider with Stack Auth. LinkedIn OAuth2 allows users to sign in to your application using their LinkedIn account. + + +## Integration Steps + + + + ### Create a LinkedIn OAuth App + + 1. Log in to the [LinkedIn Developer Portal](https://www.linkedin.com/developers/apps). + 2. Click **Create app** to create a new application. + 3. Enter your **App name** and select a **LinkedIn Page** to associate with your app (or create a new one). + 4. Upload an **App logo** (required for production apps). + 5. Enter the **App description** and your **Business email**. + 6. Check the **Legal agreement** box and click **Create app**. + 7. On your app's dashboard, click **Auth** tab from the left sidebar. + 8. Under **OAuth 2.0 settings**, add the following redirect URL: `https://api.stack-auth.com/api/v1/auth/oauth/callback/linkedin` + 9. Under **Products**, request access to **Sign In with LinkedIn** by clicking **Request access**. Complete any required information. + 10. Under **OAuth 2.0 scopes**, make sure at least the following scopes are selected: + - `r_emailaddress` + - `r_liteprofile` + 11. Once approved, navigate to the **Auth** tab again to find your **Client ID** and **Client Secret**. + + + ### Enable LinkedIn OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **LinkedIn** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from the LinkedIn Developer Portal earlier. + + + +### Need More Help? + +- Check the [LinkedIn OAuth2 Documentation](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/meta.json b/docs/content/docs/(guides)/concepts/auth-providers/meta.json new file mode 100644 index 0000000000..3c4043c3d4 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/meta.json @@ -0,0 +1,20 @@ +{ + "title": "OAuth Providers", + "defaultOpen": false, + "pages": [ + "github", + "google", + "facebook", + "microsoft", + "spotify", + "discord", + "gitlab", + "apple", + "bitbucket", + "linkedin", + "x-twitter", + "twitch", + "passkey", + "two-factor-auth" + ] +} diff --git a/docs/content/docs/(guides)/concepts/auth-providers/microsoft.mdx b/docs/content/docs/(guides)/concepts/auth-providers/microsoft.mdx new file mode 100644 index 0000000000..4972c6387d --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/microsoft.mdx @@ -0,0 +1,43 @@ +--- +title: "Microsoft" +--- + +This guide explains how to set up Microsoft as an authentication provider with Stack Auth. Microsoft OAuth allows users to sign in to your application using their Microsoft account. + + +For Development purposes, Stack Auth uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. +You should replace these before you go into production. + + +## Integration Steps + + + + ### Create a Microsoft OAuth App + + 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com/) (formerly Azure AD). + 2. In the left sidebar, go to **Applications** > **App registrations**. + 3. Click **New registration** at the top of the page. + 4. Enter a name for your application. + 5. Under **Supported account types**, select the option that best suits your needs (typically **Accounts in any organizational directory and personal Microsoft accounts**). + 6. In the **Redirect URI** section, select **Web** as the platform and enter `https://api.stack-auth.com/api/v1/auth/oauth/callback/microsoft` + 7. Click **Register** to create the application. + 8. You'll be redirected to the app's Overview page. Note the **Application (client) ID** displayed at the top. + 9. In the left sidebar, click **Certificates & secrets**. + 10. Under **Client secrets**, click **New client secret**. + 11. Add a description, select an expiration period, and click **Add**. + 12. Copy the **Value** of the client secret immediately (you won't be able to see it again). + + + ### Enable Microsoft OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Microsoft** as the provider. + 3. Set the **Client ID** (Application ID) and **Client Secret** you obtained from the Microsoft Entra admin center. + + + +### Need More Help? + +- Check the [Microsoft identity platform Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/passkey.mdx b/docs/content/docs/(guides)/concepts/auth-providers/passkey.mdx new file mode 100644 index 0000000000..937646e067 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/passkey.mdx @@ -0,0 +1,55 @@ +--- +title: "Passkey" +--- + +This guide explains how to set up Passkey authentication with Stack Auth. Passkeys allow users to sign in to your application securely using biometrics, mobile devices, or security keys. + + +Passkeys provide a more secure and convenient authentication method compared to traditional passwords by using WebAuthn standard. + + +## Integration Steps + + + + ### Enable Passkey Authentication in Stack Auth + + 1. Log in to the [Stack Auth dashboard](https://app.stack-auth.com/). + 2. Select your project from the dashboard. + 3. In the left sidebar, select **Auth Methods**. + 4. Find the **Passkey** authentication method and toggle it to enable. + 5. Save your changes. + + + ### Implement Passkey Authentication in Your Application + + 1. Make sure you've installed the Stack Auth SDK in your application: + ```bash + npm install @stackframe/stack + ``` + + 2. Add Passkey support to your sign-in component by using the built-in Stack Auth components or creating your own implementation with the SDK. + + Using built-in components: + ```jsx + import { SignIn } from "@stackframe/stack"; + + export default function SignInPage() { + return ; + } + ``` + + The built-in components will automatically show the passkey option when it's enabled in your project. + + + +## How Passkey Authentication Works + +1. **Registration**: When a user creates a new passkey, their device generates a unique public-private key pair. The private key stays on the user's device, while the public key is sent to Stack Auth's servers. + +2. **Authentication**: When a user wants to sign in, Stack Auth sends a challenge to the user's device. The device uses the private key to sign the challenge, and sends the signature back to Stack Auth for verification. + +3. **Cross-device authentication**: Users can create passkeys on one device and use them to sign in on another device using QR codes or nearby device detection. + + +For the most up-to-date compatibility information, refer to the [WebAuthn browser compatibility chart](https://caniuse.com/webauthn). diff --git a/docs/content/docs/(guides)/concepts/auth-providers/spotify.mdx b/docs/content/docs/(guides)/concepts/auth-providers/spotify.mdx new file mode 100644 index 0000000000..8529dcc85e --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/spotify.mdx @@ -0,0 +1,41 @@ +--- +title: "Spotify" +--- + +This guide explains how to set up Spotify as an authentication provider with Stack Auth. Spotify OAuth allows users to sign in to your application using their Spotify account. + + +For Development purposes, Stack Auth uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page. +You should replace these before you go into production. + + +## Integration Steps + + + + ### Create a Spotify OAuth App + + 1. Navigate to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/). + 2. Log in with your Spotify account. + 3. Click **Create app** to create a new application. + 4. Enter an **App name** and **App description**. + 5. Under **Redirect URI**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/spotify` + 6. Check the agreement checkbox and click **Create**. + 7. You'll be redirected to your app's dashboard. Note your **Client ID** displayed on this page. + 8. Click **Settings** to view more details about your app. + 9. In the settings page, you can view your **Client Secret** by clicking **Show client secret**. + 10. If needed, you can adjust the app settings, including adding additional redirect URIs. + + + ### Enable Spotify OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Spotify** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from the Spotify Developer Dashboard earlier. + + + +### Need More Help? + +- Check the [Spotify Web API Authorization Documentation](https://developer.spotify.com/documentation/general/guides/authorization/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/twitch.mdx b/docs/content/docs/(guides)/concepts/auth-providers/twitch.mdx new file mode 100644 index 0000000000..f91d8ff111 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/twitch.mdx @@ -0,0 +1,36 @@ +--- +title: "Twitch" +--- + +This guide explains how to set up Twitch as an authentication provider with Stack Auth. Twitch OAuth allows users to sign in to your application using their Twitch account. + +## Integration Steps + + + + ### Create a Twitch OAuth App + + 1. Navigate to the [Twitch Developer Console](https://dev.twitch.tv/console). + 2. Log in with your Twitch account. + 3. Navigate to **Applications** and click **Register New Application**. + 4. Enter a **Name** and select a **Category**. + 5. Under **OAuth Redirect URLs**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/twitch` + 6. Click **Create**. + 7. You'll be redirected to your app's dashboard. + 8. Click **Manage** of the app you just created to view more details about your app. + 9. Click "New Secret" to generate a new secret. + 10. Copy and save the **Client ID** and **Client Secret**. + + + ### Enable Twitch OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **Twitch** as the provider. + 3. Set the **Client ID** and **Client Secret** you obtained from the Twitch Developer Console earlier. + + + +### Need More Help? + +- Check the [Twitch OAuth Documentation](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/two-factor-auth.mdx b/docs/content/docs/(guides)/concepts/auth-providers/two-factor-auth.mdx new file mode 100644 index 0000000000..1ca7453c0a --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/two-factor-auth.mdx @@ -0,0 +1,59 @@ +--- +title: "Two-Factor Authentication (2FA)" +--- + +This guide explains how Two-Factor Authentication (2FA) works with Stack Auth. 2FA adds an extra layer of security by requiring users to provide a verification code in addition to their password. + + +Stack Auth implements TOTP (Time-based One-Time Password) for two-factor authentication, which is compatible with standard authenticator apps like Google Authenticator, Microsoft Authenticator, and Authy. 2FA is enabled by default at the platform level and can be configured by individual users. + + +## Integration Steps + + + + ### No Developer Configuration Required + + 2FA is enabled by default on the Stack Auth platform. Unlike other authentication methods, you don't need to enable it specifically for your project. + + + ### Implement User Settings in Your Application + + To allow your users to set up 2FA for their accounts: + + 1. Make sure you've installed the Stack Auth SDK in your application: + ```bash + npm install @stackframe/stack + ``` + + 2. Use the Stack Auth components to give users access to their account settings, where they can enable 2FA: + + ```jsx + import { AccountSettings } from "@stackframe/stack"; + + export default function SettingsPage() { + return ; + } + ``` + + 3. The built-in Stack Auth components will handle the entire 2FA setup process, including QR code generation, verification, and recovery codes. + + + +## How Stack Auth 2FA Works + +Stack Auth uses the industry-standard TOTP (Time-based One-Time Password) algorithm for two-factor authentication: + +1. **User Setup**: When a user enables 2FA in their account settings, Stack Auth generates a secret key that is shared with the user's authenticator app (usually via a QR code). + +2. **Code Generation**: The authenticator app generates a 6-digit code that changes every 30 seconds, based on the shared secret and the current time. + +## Recommended Authenticator Apps + +The following authenticator apps are compatible with Stack Auth 2FA: + +- Google Authenticator (Android, iOS) +- Microsoft Authenticator (Android, iOS) +- Authy (Android, iOS, desktop) +- 1Password (Android, iOS, desktop) +- LastPass Authenticator (Android, iOS) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx b/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx new file mode 100644 index 0000000000..a583febbca --- /dev/null +++ b/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx @@ -0,0 +1,43 @@ +--- +title: "X (Twitter)" +--- + +This guide explains how to set up X (formerly Twitter) as an authentication provider with Stack Auth. X OAuth 2.0 allows users to sign in to your application using their X account. + + +## Integration Steps + + + + ### Create an X Developer Account and Project + + 1. Log in to the [X Developer Portal](https://developer.twitter.com/). + 2. Navigate to the [Developer Portal Dashboard](https://developer.twitter.com/en/portal/dashboard). + 3. Click on **+ Create Project** to create a new project. + 4. Enter a name for your project and select **Web App, Automated App or Bot** as the use case, then click **Next**. + 5. Enter a description for your project and click **Next**. + 6. Name your app and click **Next**. + 7. In the **App settings** section, find your API Key and Secret. These will serve as your OAuth 2.0 Client ID and Client Secret. + 8. In the left sidebar, click on your project, then select the app you just created. + 9. Click on the **Settings** tab and scroll to the **User authentication settings**. + 10. Click **Set up** or **Edit** if already configured. + 11. Enable **OAuth 2.0** and set the following details: + - **Type of App**: Web App + - **Callback URL / Redirect URL**: `https://api.stack-auth.com/api/latest/auth/oauth/callback/x` + - **Website URL**: Your website's URL + 12. Under **App permissions**, select your scopes. + 13. Click **Save** to apply your changes. + + + ### Enable X OAuth in Stack Auth + + 1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar. + 2. Click **Add SSO Providers** and select **X (Twitter)** as the provider. + 3. Set the **Client ID** (your API Key) and **Client Secret** you obtained from the X Developer Portal earlier. + + + +### Need More Help? + +- Check the [X OAuth 2.0 documentation](https://developer.twitter.com/en/docs/authentication/oauth-2-0) +- Join our [Discord](https://discord.stack-auth.com) diff --git a/docs/content/docs/(guides)/concepts/backend-integration.mdx b/docs/content/docs/(guides)/concepts/backend-integration.mdx new file mode 100644 index 0000000000..e91d035d64 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/backend-integration.mdx @@ -0,0 +1,135 @@ +--- +title: Backend Integration +description: Integrate Stack Auth with your own server with the REST APIs +--- + +To authenticate your endpoints, you need to send the user's access token in the headers of the request to your server, and then make a request to Stack's server API to verify the user's identity. + +## Sending requests to your server endpoints + +To authenticate your own server endpoints using Stack's server API, you need to protect your endpoints by sending the user's access token in the headers of the request. + +On the client side, you can retrieve the access token from the `user` object by calling `user.getAuthJson()`. This will return an object containing `accessToken`. + +Then, you can call your server endpoint with these two tokens in the headers, like this: + +```typescript +const { accessToken } = await user.getAuthJson(); +const response = await fetch('/api/users/me', { + headers: { + 'x-stack-access-token': accessToken, + }, + // your other options and parameters +}); +``` + +## Authenticating the user on the server endpoints + +Stack Auth provides two methods for authenticating users on your server endpoints: + +1. **JWT Verification**: A fast, lightweight approach that validates the user's token locally without making external requests. While efficient, it provides only essential user information encoded in the JWT. +2. **REST API Verification**: Makes a request to Stack Auth's servers to validate the token and retrieve comprehensive user information. This method provides access to the complete, up-to-date user profile. + +### Using JWT + + + + Node.js + Python + + + ```javascript + // you need to install the jose library if it's not already installed + import * as jose from 'jose'; + + // you can cache this and refresh it with a low frequency + const jwks = jose.createRemoteJWKSet(new URL("https://api.stack-auth.com/api/v1/projects//.well-known/jwks.json")); + + const accessToken = 'access token from the headers'; + + try { + const { payload } = await jose.jwtVerify(accessToken, jwks); + console.log('Authenticated user with ID:', payload.sub); + } catch (error) { + console.error(error); + console.log('Invalid user'); + } + ``` + + + ```python + # you need to install PyJWT and cryptography libraries if they're not already installed + # pip install PyJWT[crypto] requests + + import jwt + import requests + from jwt import PyJWKClient + from jwt.exceptions import InvalidTokenError + + # you can cache this and refresh it with a low frequency + jwks_client = PyJWKClient("https://api.stack-auth.com/api/v1/projects//.well-known/jwks.json") + + access_token = 'access token from the headers' + + try: + signing_key = jwks_client.get_signing_key_from_jwt(access_token) + payload = jwt.decode( + access_token, + signing_key.key, + algorithms=["ES256"], + audience= + ) + print('Authenticated user with ID:', payload['sub']) + except Exception as error: + print(error) + print('Invalid user') + ``` + + + + +### Using the REST API + + + + Node.js + Python + + + ```javascript + const url = 'https://api.stack-auth.com/api/v1/users/me'; + const headers = { + 'x-stack-access-type': 'server', + 'x-stack-project-id': 'generated on the Stack Auth dashboard', + 'x-stack-secret-server-key': 'generated on the Stack Auth dashboard', + 'x-stack-access-token': 'access token from the headers', + }; + + const response = await fetch(url, { headers }); + if (response.status === 200) { + console.log('User is authenticated', await response.json()); + } else { + console.log('User is not authenticated', response.status, await response.text()); + } + ``` + + + ```python + import requests + + url = 'https://api.stack-auth.com/api/v1/users/me' + headers = { + 'x-stack-access-type': 'server', + 'x-stack-project-id': 'generated on the Stack Auth dashboard', + 'x-stack-secret-server-key': 'generated on the Stack Auth dashboard', + 'x-stack-access-token': 'access token from the headers', + } + + response = requests.get(url, headers=headers) + if response.status_code == 200: + print('User is authenticated', response.json()) + else: + print('User is not authenticated', response.status_code, response.text) + ``` + + diff --git a/docs/content/docs/(guides)/concepts/custom-user-data.mdx b/docs/content/docs/(guides)/concepts/custom-user-data.mdx new file mode 100644 index 0000000000..86a5965f87 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/custom-user-data.mdx @@ -0,0 +1,58 @@ +--- +title: Custom User Data +description: How to store custom user metadata in Stack Auth +--- + +Stack Auth allows storing additional user information through three types of metadata fields: + +1. **clientMetadata**: Readable and writable from a [client](../concepts/stack-app#client-vs-server). +2. **serverMetadata**: Readable and writable only from a [server](../concepts/stack-app#client-vs-server). +3. **clientReadOnlyMetadata**: Readable from a client, writable only from a server. + +## Client metadata +You can use the `clientMetadata` field to store non-sensitive information that both the client and server can read and write. + +```tsx +await user.update({ + clientMetadata: { + mailingAddress: "123 Main St", + }, +}); + +// On the client: +const user = useUser(); +console.log(user.clientMetadata); +``` + +## Server-side metadata +For sensitive information, use the `serverMetadata` field. This ensures the data is only accessible and modifiable by the server. + +```tsx +const user = await stackServerApp.getUser(); +await user.update({ + serverMetadata: { + secretInfo: "This is a secret", + }, +}); + +// To read: +const user = await stackServerApp.getUser(); +console.log(user.serverMetadata); +``` + +## Client read-only metadata +Use `clientReadOnlyMetadata` for data that clients need to read but never modify, such as subscription status. + +```tsx +// On the server: +const user = await stackServerApp.getUser(); +await user.update({ + clientReadOnlyMetadata: { + subscriptionPlan: "premium", + }, +}); + +// On the client: +const user = useUser(); +console.log(user.clientReadOnlyMetadata); +``` diff --git a/docs/content/docs/(guides)/concepts/emails.mdx b/docs/content/docs/(guides)/concepts/emails.mdx new file mode 100644 index 0000000000..b5a12fa5a4 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/emails.mdx @@ -0,0 +1,197 @@ +--- +title: Emails +description: Send custom emails to your users with Stack Auth's email system. +--- + +Stack Auth provides emails that allows you to send custom emails to your users. The system supports both custom HTML emails and template-based emails with theming. + +## Email Types: +There are two types of emails that you can send to your users: +- **Transactional Emails**: Transactional emails are those required for your user to use your application. These emails cannot be opted out of. +- **Marketing Emails**: Marketing emails always contain an unsubscribe link and may be more general marketing material related to your application/company. + + +Never send marketing emails as transactional emails, as this can quickly lead to your domain being blacklisted by email spam filters. + + + + +## Overview + +The email system provides: + +- **Email Sending**: Send custom emails to users via the `sendEmail` method on `StackServerApp` +- **Email Templates**: Use predefined email templates for common authentication flows +- **Email Themes**: Apply consistent styling to your emails +- **Notification Categories**: Allow users to control which emails they receive + +## Server-Side Email Sending + +### Basic Email Sending + +Use the `sendEmail` method on your server app to send emails to users: + +```typescript +import { stackServerApp } from './stack'; + +// Send a custom HTML email +const result = await stackServerApp.sendEmail({ + userIds: ['user-id-1', 'user-id-2'], + subject: 'Welcome to our platform!', + html: '

Welcome!

Thanks for joining us.

', +}); + +if (result.status === 'error') { + console.error('Failed to send email:', result.error); +} +``` + +### Template-Based Emails + +Send emails using predefined templates with variables: + +```typescript +// Send email using a template +const result = await stackServerApp.sendEmail({ + userIds: ['user-id'], + templateId: 'welcome-template', + subject: 'Welcome to our platform!', + variables: { + userName: 'John Doe', + activationUrl: 'https://yourapp.com/activate/token123', + supportEmail: 'support@yourapp.com', + }, +}); +``` + +### Email Options + +The `sendEmail` method accepts the following options: + +```typescript +type SendEmailOptions = { + userIds: string[]; // Array of user IDs to send to + themeId?: string | null | false; // Theme to apply (optional) + subject?: string; // Email subject + notificationCategoryName?: string; // Notification category for user preferences + html?: string; // Custom HTML content + templateId?: string; // Template ID to use + variables?: Record; // Template variables +}; +``` + +### Error Handling + +The `sendEmail` method returns a `Result` type that can contain specific errors: + +```typescript +const result = await stackServerApp.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; + } +} +``` + + + +## Built-in Email Templates + +Stack Auth provides several built-in email templates for common authentication flows: + +- **Email Verification**: `email_verification` - Sent when users need to verify their email +- **Password Reset**: `password_reset` - Sent when users request password reset +- **Magic Link**: `magic_link` - Sent for passwordless authentication +- **Team Invitation**: `team_invitation` - Sent when users are invited to teams +- **Sign-in Invitation**: `sign_in_invitation` - Sent to invite users to sign up + +These templates can be customized through the admin interface or programmatically. + +## Email Configuration + +Email configuration is managed through the Stack Auth dashboard or admin API, not directly in your application code. You have two options: + +### Shared Email Provider (Development) + +For development and testing, you can use Stack's shared email provider. This sends emails from `noreply@stackframe.co` and is configured through your project settings in the Stack Auth dashboard. + +- Go to your project's Email settings in the dashboard +- Select "Shared" as your email server type +- No additional configuration required + +### Custom Email Server (Production) + +For production, configure your own SMTP server through the dashboard: + +- Go to your project's Email settings in the dashboard +- Select "Custom SMTP server" as your email server type +- Configure the following settings: + - **Host**: Your SMTP server hostname (e.g., `smtp.yourprovider.com`) + - **Port**: SMTP port (typically 587 for TLS or 465 for SSL) + - **Username**: Your SMTP username + - **Password**: Your SMTP password + - **Sender Email**: The email address emails will be sent from + - **Sender Name**: The display name for your emails + +The dashboard will automatically test your configuration when you save it. + +## Notification Categories + +Control which emails users receive by organizing them into notification categories: + +```typescript +await stackServerApp.sendEmail({ + userIds: ['user-id'], + html: '

New feature available!

', + subject: 'Product Updates', + notificationCategoryName: 'product_updates', +}); +``` + +Users can then opt in or out of specific notification categories through their account settings. + +## Best Practices + +1. **Use Templates**: Leverage built-in templates for consistent branding and easier maintenance +2. **Handle Errors**: Always check the result status and handle potential errors +3. **Respect User Preferences**: Use notification categories to let users control what emails they receive +4. **Server-Side Only**: Always send emails from your server-side code, never from the client + +## React Components Integration + +Emails integrates seamlessly with Stack Auth's React components. Email verification, password reset, and other authentication emails are automatically sent when users interact with the provided components. + +For custom email flows, use the `sendEmail` method from your server-side code: + +```typescript +// In your API route or server action +import { stackServerApp } from '@stackframe/stack'; + +export async function inviteUser(email: string) { + const result = await stackServerApp.sendEmail({ + userIds: [userId], // Get user ID first + templateId: 'invitation-template', + subject: 'You\'re invited!', + variables: { + inviteUrl: 'https://yourapp.com/invite/token123', + }, + }); + + return result; +} +``` + +This email system gives you control over your application's email communications while maintaining the security and reliability of Stack Auth's infrastructure. diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx new file mode 100644 index 0000000000..3cfec2e827 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/jwt.mdx @@ -0,0 +1,224 @@ +--- +title: JWT Tokens +--- + +JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties. Stack Auth uses JWTs for secure authentication and authorization. + +## What is a JWT? + +A JWT is a string that consists of three parts separated by dots (`.`): + +1. **Header**: Contains metadata about the token, such as the signing algorithm +2. **Payload**: Contains the claims (data) about the user or entity +3. **Signature**: Used to verify the token's authenticity + +The structure looks like this: `header.payload.signature` + +## JWT Viewer + +Use the interactive JWT viewer below to decode and inspect JWT tokens. If you're signed in, it will automatically load and display your current session token: + + + +## Stack Auth JWT Structure + +Stack Auth JWTs contain standardized headers and claims that power authentication throughout the platform. + +### Header + +- **`alg`**: Always `ES256` +- **`kid`**: Identifies which public key from the JWKS should be used for verification + +### Standard Claims + +- **`iss` (Issuer)**: `https://api.stack-auth.com/api/v1/projects/` for regular users, or `https://api.stack-auth.com/api/v1/projects-anonymous-users/` for anonymous sessions +- **`sub` (Subject)**: The user ID this token represents +- **`aud` (Audience)**: The intended recipient of the token — `` for regular sessions, `:anon` for anonymous sessions +- **`exp` (Expiration)**: When the token expires (Unix timestamp) +- **`iat` (Issued At)**: When the token was issued (Unix timestamp) + +### Stack Auth Specific Claims + +- **`project_id`**: Your Stack Auth project ID +- **`branch_id`**: The project branch (currently always `main`) +- **`refresh_token_id`**: ID of the associated refresh token +- **`role`**: Always set to `authenticated` for valid users +- **`name`**: The user's display name (nullable) +- **`email`**: The user's primary email address (nullable) +- **`email_verified`**: Whether the user's email has been verified +- **`selected_team_id`**: The currently selected team ID (nullable) +- **`is_anonymous`**: Whether this is an anonymous user session + +## Example JWT Payload + +Here's what a typical Stack Auth JWT payload looks like: + +```json +{ + "iss": "https://api.stack-auth.com/api/v1/projects/project_abcdef", + "sub": "user_123456", + "aud": "project_abcdef", + "exp": 1735689600, + "iat": 1735603200, + "project_id": "project_abcdef", + "branch_id": "main", + "refresh_token_id": "refresh_xyz789", + "role": "authenticated", + "name": "John Doe", + "email": "john@example.com", + "email_verified": true, + "selected_team_id": "team_789", + "is_anonymous": false +} +``` + +Anonymous user tokens have the same shape, but: + +- `iss` becomes `https://api.stack-auth.com/api/v1/projects-anonymous-users/` +- `aud` becomes `:anon` +- `is_anonymous` is `true` + +## Working with JWTs + +### Client-Side Usage + +Stack Auth automatically handles JWT tokens for you. When you use hooks like `useUser()`, the JWT is automatically included in API requests: + +```tsx +import { useUser } from '@stackframe/stack'; + +export function UserProfile() { + const user = useUser(); + + if (!user) { + return
Please sign in
; + } + + return
Welcome, {user.displayName}!
; +} +``` + +### Server-Side Usage + +On the server side, you can access the JWT and its claims through the Stack Auth API: + +```tsx +import { stackServerApp } from '@/stack'; + +export async function GET() { + const user = await stackServerApp.getUser(); + + if (!user) { + return new Response('Unauthorized', { status: 401 }); + } + + // Access user information from the JWT + return Response.json({ + id: user.id, + displayName: user.displayName, + primaryEmail: user.primaryEmail, + selectedTeamId: user.selectedTeamId, + // Other user properties... + }); +} +``` + +### Manual JWT Verification + +If you need to manually verify a JWT (for example, in a different service), fetch the public keys from Stack Auth's JWKS endpoint. Keys are derived per audience so the `kid` in the JWT header always matches one of the published keys. + +```typescript +import * as jose from 'jose'; + +// Get the public key set from Stack Auth +const jwks = jose.createRemoteJWKSet( + new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json') +); + +// Verify a regular (non-anonymous) access token +try { + const { payload } = await jose.jwtVerify(token, jwks, { + issuer: 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', + audience: 'YOUR_PROJECT_ID', + }); + + console.log('JWT is valid:', payload); +} catch (error) { + console.error('JWT verification failed:', error); +} +``` + +To support anonymous sessions, include those keys and allow both issuers and audiences: + +```typescript +const jwks = jose.createRemoteJWKSet( + new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true') +); + +const { payload } = await jose.jwtVerify(token, jwks, { + issuer: [ + 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', + 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID', + ], + audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'], +}); +``` + +### Signing Keys + +- Private keys are deterministically derived from your project ID, optional anonymous audience, and the `STACK_SERVER_SECRET` environment variable. This means no key material is ever stored in the database. +- The JWKS currently exposes both the latest key pair and a legacy compatibility key. Verification libraries automatically pick the correct key by matching the `kid` provided in the JWT header. +- Tokens are always signed server-side; client SDKs never receive the private keys. + +## Security Considerations + +### Token Storage + +- **Never store JWTs in localStorage** for sensitive applications +- Use secure, httpOnly cookies when possible +- Stack Auth handles secure token storage automatically + +### Token Expiration + +- JWTs have a limited lifetime (default is 10 minutes via `STACK_ACCESS_TOKEN_EXPIRATION_TIME`) +- Stack Auth automatically refreshes tokens before they expire +- Always check the `exp` claim when manually handling JWTs + +### Signature Verification + +- Always verify JWT signatures using the public key +- Never trust the contents of a JWT without verification +- Stack Auth SDKs handle verification automatically + +## Troubleshooting + +### Common Issues + +1. **"JWT is expired"**: The token has passed its expiration time. Stack Auth will automatically refresh it. + +2. **"Invalid signature"**: The token was tampered with or signed with a different key. + +3. **"Invalid audience"**: The token was issued for a different project or environment. + +### Debugging JWTs + +Use the JWT viewer above to inspect tokens and verify their contents. Pay special attention to: + +- Expiration times (`exp` claim) +- Audience (`aud` claim) matching your project +- Required claims are present + +## Best Practices + +1. **Let Stack Auth handle tokens**: Use the provided SDKs instead of manual JWT handling +2. **Validate on the server**: Always verify JWTs on your backend +3. **Check expiration**: Ensure tokens haven't expired before using them +4. **Use HTTPS**: Always transmit JWTs over secure connections +5. **Monitor token usage**: Log authentication events for security monitoring + +## Related Concepts + +- [API Keys](/docs/concepts/api-keys) - Alternative authentication method for server-to-server communication +- [Backend Integration](/docs/concepts/backend-integration) - How to verify JWTs in your backend +- [Permissions](/docs/concepts/permissions) - Understanding user permissions (not included in JWTs) +- [Teams](/docs/concepts/teams) - Understanding team context in JWTs diff --git a/docs/content/docs/(guides)/concepts/oauth.mdx b/docs/content/docs/(guides)/concepts/oauth.mdx new file mode 100644 index 0000000000..a6c3536641 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/oauth.mdx @@ -0,0 +1,118 @@ +--- +title: OAuth +description: Managing third-party OAuth access tokens +--- + +Stack has good support for working with OAuth and OIDC providers, such as Google, Facebook, Microsoft, and others. + +Beyond using OAuth for signing in, Stack can manage your users' access token so you can invoke APIs on their behalf. For example, you can use this to send emails with Gmail, access repositories on GitHub, or access files on OneDrive. + +A connected account is simply an external account that is linked to the user in some way. If you are not using shared keys (see note below), any user created with "Sign up with OAuth" is automatically connected to the account they signed up with, but it's also possible to connect a user with a provider that is unavailable for sign in. + + + You cannot connect a user's accounts with shared OAuth keys. You need to set up your own OAuth client ID and client secret in Stack's dashboard. For more details, check [Going to Production](../getting-started/production#oauth-providers). + + +## Connecting with OAuth providers + +You can access a user's connected account with the `user.getConnectedAccount(providerId)` function or `user.useConnectedAccount(providerId)` hook. + +Often, you'll want to redirect the user to the OAuth provider's authorization page if they have not connected the account yet. Just like the `getUser(...)` function, `getConnectedAccount(...)` can also take an `{ or: "redirect" }` argument to achieve this. + +Here's how to connect with Google: + +```jsx +'use client'; + +import { useUser } from "@stackframe/stack"; + +export default function Page() { + const user = useUser({ or: 'redirect' }); + // Redirects to Google authorization if not already connected + const account = user.useConnectedAccount('google', { or: 'redirect' }); + // Account is always defined because of the redirect + return
Google account connected
; +} +``` + + +## Providing scopes + +Most providers have access control in the form of OAuth scopes. These are the permissions that the user will see on the authorization screen (eg. "Your App wants access to your calendar"). For instance, to read Google Drive content, you need the `https://www.googleapis.com/auth/drive.readonly` scope: + +```jsx +'use client'; + +import { useUser } from "@stackframe/stack"; + +export default function Page() { + const user = useUser({ or: 'redirect' }); + // Redirects to the Google authorization page, requesting access to Google Drive + const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/authdrive.readonly'] }); + // Account is always defined because of the redirect + return
Google Drive connected
; +} +``` + +Check your provider's API documentation to find a list of available scopes. + +## Retrieving the access token + +Once connected with an OAuth provider, obtain the access token with the `account.getAccessToken()` function. Check your provider's API documentation to understand how you can use this token to authorize the user in requests. + +```jsx +'use client'; + +import { useEffect, useState } from 'react'; +import { useUser } from "@stackframe/stack"; + +export default function Page() { + const user = useUser({ or: 'redirect' }); + const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/auth/drive.readonly'] }); + const { accessToken } = account.useAccessToken(); + const [response, setResponse] = useState(); + + useEffect(() => { + fetch('https://www.googleapis.com/drive/v3/files', { + headers: { Authorization: `Bearer ${accessToken}` } + }) + .then((res) => res.json()) + .then((data) => setResponse(data)) + .catch((err) => console.error(err)); + }, [accessToken]); + + return
{response ? JSON.stringify(response) : 'Loading...'}
; +} +``` + +## Sign-in default scopes + +To avoid showing the authorization page twice, you can already request scopes during the sign-in flow. This approach is optional. Some applications may prefer to request extra permissions only when needed, while others might want to obtain all necessary permissions upfront. + +To do this, edit the `oauthScopesOnSignIn` setting of your `stackServerApp`: + +```jsx title='stack/server.ts' +export const stackServerApp = new StackServerApp({ + // ...your other settings... + oauthScopesOnSignIn: { + google: ['https://www.googleapis.com/authdrive.readonly'] + } +}); +``` + +## OAuth account merging strategies + +When a user attempts to sign in with an OAuth provider that matches an existing account, Stack provides different strategies for handling the authentication flow. + +The available strategies are: + +- Allow duplicates (legacy default) +- Link method (new default) +- Block duplicates (most secure) + +The "Link" strategy is the default behavior. If a user attempts to sign in with an OAuth provider that matches an existing account, Stack will link the OAuth identity to the existing account, and the user will be signed into that account. +This requires both of the credentials to be verified, or otherwise the link will be blocked, in the same way as the "Block" strategy. + +The "Allow" strategy is the default behavior for old projects. If a user attempts to sign in with an OAuth provider that has an existing account with the same email address, Stack will create a separate account for the user. + +The "Block" strategy will explicitly raise an error if a user attempts to sign in with an OAuth provider that matches an existing account. diff --git a/docs/content/docs/(guides)/concepts/orgs-and-teams.mdx b/docs/content/docs/(guides)/concepts/orgs-and-teams.mdx new file mode 100644 index 0000000000..1319465288 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/orgs-and-teams.mdx @@ -0,0 +1,246 @@ +--- +title: Orgs and Teams +description: Manage teams and team members +--- + +Teams provide a structured way to group users and manage their permissions. Users can belong to multiple teams simultaneously, allowing them to represent departments, B2B customers, or projects. + +The server can perform all operations on a team, but the client can only carry out some actions if the user has the necessary permissions. This applies to all actions that can be performed on a server/client-side `User` object and a `Team` object. + +## Concepts + +### Team permissions + +If you attempt to perform an action without the necessary team permissions, the function will throw an error. Always check if the user has the required permission before performing any action. Learn more about permissions [here](./permissions.mdx). + +Here is an example of how to check if a user has a specific permission on the client + +```tsx +const user = useUser({ or: 'redirect' }); +const team = user.useTeam('some-team-id'); + +if (!team) { + return
Team not found
; +} + +const hasPermission = user.usePermission(team, '$invite_members'); + +if (!hasPermission) { + return
No permission
; +} + +// Perform corresponding action like inviting a user +``` + +### Team profile + +A user can have a different profile for each team they belong to (Note this is different to the user's personal profile). This profile contains information like `displayName` and `profileImageUrl`. The team profile can be left empty and it will automatically take the user's personal profile information. + +The team profile is visible to all the other users in the team that have the `$read_members` permission. + +## Retrieving a user's teams + +You can list all teams a user belongs to using the `listTeams` or `useTeams` functions or fetch a specific team with `getTeam` or `useTeam`. These functions work on both clients and servers. + + + + Client Component + Server Component + + + ```tsx + const user = useUser({ or: 'redirect' }); + const allTeams = user.useTeams(); + const someTeam = user.useTeam('some-team-id'); // May be null if the user is not a member of this team + + return ( +
+ {allTeams.map(team => ( +
{team.displayName}
+ ))} +
+
+ {someTeam ? someTeam.displayName : 'Not a member of this team'} +
+ ); + ``` +
+ + ```tsx + const user = await stackServerApp.getUser({ or: 'redirect' }); + const allTeams = await user.listTeams(); + const someTeam = await user.getTeam('some-team-id'); // May be null if the user is not a member of this team + + return ( +
+ {allTeams.map(team => ( +
{team.displayName}
+ ))} +
+
+ {someTeam ? someTeam.displayName : 'Not a member of this team'} +
+ ``` +
+
+ +## Creating a team + +To create a team, use the `createTeam` function on the `User` object. The user will be added to the team with the default team creator permissions (You can change this on the permissions tab in the Stack dashboard). + +On the client side, this requires enabling the "client side team creation" on the team settings tab in the Stack dashboard. + +```jsx +const team = await user.createTeam({ + displayName: 'New Team', +}); +``` + +To create a team on the server without adding a specific user, use the `createTeam` function on the `ServerApp` object: + +```jsx +const team = await stackServerApp.createTeam({ + displayName: 'New Team', +}); +``` + +## Updating a team + +You can update a team with the `update` function on the `Team` object. + +On the client, the user must have the `$update_team` permission to perform this action. + +```tsx +await team.update({ + displayName: 'New Name', +}); +``` + +## Custom team metadata + +You can store custom metadata on a team object, similar to the user object. The metadata can be any JSON object. + +- `clientMetadata`: Can be read and updated on both the client and server sides. +- `serverMetadata`: Can only be read and updated on the server side. +- `clientReadOnlyMetadata`: Can be read on both the client and server sides, but can only be updated on the server side. + +```tsx +await team.update({ + clientMetadata: { + customField: 'value', + }, +}); + +console.log(team.clientMetadata.customField); // 'value' +``` + +## List users in a team + +You can list all users in a team with the `listUsers` function or the `useUsers` hook on the `Team` object. Note that if you want to get the team profile, you need to get it with `user.teamProfile`. + +On the client, the current user must have the `$read_members` permission in the team to perform this action. + + + + Client Component + Server Component + + + ```tsx + // ... retrieve the team and ensure user has the necessary permissions + + const users = team.useUsers(); + + return ( +
+ {users.map(user => ( +
{user.teamProfile.displayName}
+ ))} +
+ ); + ``` +
+ + ```tsx + // ... retrieve the team + + const users = await team.listUsers(); + + return ( +
+ {users.map(user => ( +
{user.teamProfile.displayName}
+ ))} +
+ ); + ``` +
+
+ +## Get current user's team profile + +You can get the current user's team profile with the `getTeamProfile` or `useTeamProfile` function on the `User` object. This function returns the team profile for the team with the given ID. + + + + Client Component + Server Component + + + ```tsx + const teamProfile = user.useTeamProfile(team); + ``` + + + ```tsx + const teamProfile = await user.getTeamProfile(team); + ``` + + + +## Invite a user to a team + +You can invite a user to a team using the `inviteUser` function on the `Team` object. The user will receive an email with a link to join the team. + +On the client side, the current user must have the `$invite_members` permission to perform this action. + +```tsx +await team.inviteUser(email); +``` + +## Adding a user to a team + +If you want to add a user to a team without sending an email, use the `addUser` function on the `ServerTeam` object. This function can only be called on the server side. + +```tsx +await team.addUser(user.id); +``` + +## Removing a user from a team + +You can remove a user from a team with the `removeUser` function on the `Team` object. + +On the client side, the current user must have the `$remove_members` permission to perform this action. + +```tsx +await team.removeUser(user.id); +``` + +## Leaving a team + +All users can leave a team without any permissions required. + +```tsx +const team = await user.getTeam('some-team-id'); +await user.leaveTeam(team); +``` + +## Deleting a team + +You can delete a team with the `delete` function on the `Team` object. + +On the client side, the current user must have the `$delete_team` permission to perform this action. + +```tsx +await team.delete(); +``` diff --git a/docs/content/docs/(guides)/concepts/permissions.mdx b/docs/content/docs/(guides)/concepts/permissions.mdx new file mode 100644 index 0000000000..778ad912f1 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/permissions.mdx @@ -0,0 +1,264 @@ +--- +title: Permissions +--- + +Permissions are a way to control what each user can do and access within your application. + +## Permission Types + +Stack 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 + +Both permission types can be managed from the dashboard, and both support arbitrary nesting. + +## 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. + +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. + +### 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. + +### 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: + + + + Client Component + Server Component + + + ```tsx title="Check user permission on the client" + "use client"; + import { useUser } from "@stackframe/stack"; + + export function CheckUserPermission() { + const user = useUser({ or: 'redirect' }); + const team = user.useTeam('some-team-id'); + const permission = user.usePermission(team, 'read'); + + // Don't rely on client-side permission checks for business logic. + return ( +
+ {permission ? 'You have the read permission' : 'You shall not pass'} +
+ ); + } + ``` +
+ + ```tsx title="Check user permission on the server" + import { stackServerApp } from "@/stack/server"; + + export default async function CheckUserPermission() { + const user = await stackServerApp.getUser({ or: 'redirect' }); + const team = await stackServerApp.getTeam('some-team-id'); + const permission = await user.getPermission(team, 'read'); + + // This is a server-side check, so it's secure. + return ( +
+ {permission ? 'You have the read permission' : 'You shall not pass'} +
+ ); + } + ``` +
+
+ +### 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: + + + + Client Component + Server Component + + + ```tsx title="List user permissions on the client" + "use client"; + import { useUser } from "@stackframe/stack"; + + export function DisplayUserPermissions() { + const user = useUser({ or: 'redirect' }); + const permissions = user.usePermissions(); + + return ( +
+ {permissions.map(permission => ( +
{permission.id}
+ ))} +
+ ); + } + ``` +
+ + ```tsx title="List user permissions on the server" + import { stackServerApp } from "@/stack/server"; + + export default async function DisplayUserPermissions() { + const user = await stackServerApp.getUser({ or: 'redirect' }); + const permissions = await user.listPermissions(); + + return ( +
+ {permissions.map(permission => ( +
{permission.id}
+ ))} +
+ ); + } + ``` +
+
+ +### Granting a Permission to a User + +To grant a permission to a user, use the `grantPermission` method on the `ServerUser`. Here's an example: + +```tsx +const team = await stackServerApp.getTeam('teamId'); +const user = await stackServerApp.getUser(); +await user.grantPermission(team, 'read'); +``` + +### Revoking a Permission from a User + +To revoke a permission from a user, use the `revokePermission` method on the `ServerUser`. Here's an example: + +```tsx +const team = await stackServerApp.getTeam('teamId'); +const user = await stackServerApp.getUser(); +await user.revokePermission(team, 'read'); +``` + +## Project Permissions + +Project permissions are global permissions that apply to a user across the entire project, regardless of team context. These permissions are useful for handling things like premium plan subscriptions or global admin access. + +### Creating a Project Permission + +To create a new project permission, navigate to 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. + +### 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: + + + + Client Component + Server Component + + + ```tsx title="Check user permission on the client" + "use client"; + import { useUser } from "@stackframe/stack"; + + export function CheckGlobalPermission() { + const user = useUser({ or: 'redirect' }); + const permission = user.usePermission('access_admin_dashboard'); + + return ( +
+ {permission ? 'You can access the admin dashboard' : 'Access denied'} +
+ ); + } + ``` +
+ + ```tsx title="Check user permission on the server" + import { stackServerApp } from "@/stack/server"; + + export default async function CheckGlobalPermission() { + const user = await stackServerApp.getUser({ or: 'redirect' }); + const permission = await user.getPermission('access_admin_dashboard'); + + return ( +
+ {permission ? 'You can access the admin dashboard' : 'Access denied'} +
+ ); + } + ``` +
+
+ +### Listing All Project Permissions + +To get a list of all global permissions a user has, use the `listPermissions` method or the `usePermissions` hook: + + + + Client Component + Server Component + + + ```tsx title="List global permissions on the client" + "use client"; + import { useUser } from "@stackframe/stack"; + + export function DisplayGlobalPermissions() { + const user = useUser({ or: 'redirect' }); + const permissions = user.usePermissions(); + + return ( +
+ {permissions.map(permission => ( +
{permission.id}
+ ))} +
+ ); + } + ``` +
+ + ```tsx title="List global permissions on the server" + import { stackServerApp } from "@/stack/server"; + + export default async function DisplayGlobalPermissions() { + const user = await stackServerApp.getUser({ or: 'redirect' }); + const permissions = await user.listPermissions(); + + return ( +
+ {permissions.map(permission => ( +
{permission.id}
+ ))} +
+ ); + } + ``` +
+
+ +### Granting a Project Permission + +To grant a global permission to a user, use the `grantPermission` method: + +```tsx +const user = await stackServerApp.getUser(); +await user.grantPermission('access_admin_dashboard'); +``` + +### Revoking a Project Permission + +To revoke a global permission from a user, use the `revokePermission` method: + +```tsx +const user = await stackServerApp.getUser(); +await user.revokePermission('access_admin_dashboard'); +``` + +By following these guidelines, you can efficiently manage and verify both team and user permissions within your application. diff --git a/docs/content/docs/(guides)/concepts/stack-app.mdx b/docs/content/docs/(guides)/concepts/stack-app.mdx new file mode 100644 index 0000000000..44df877867 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/stack-app.mdx @@ -0,0 +1,50 @@ +--- +title: Stack App +description: The most important object of your Stack project +--- + +By now, you may have seen the `useStackApp()` hook and the `stackServerApp` variable. Both return a `StackApp`, of type `StackClientApp` and `StackServerApp` respectively. + +Nearly all of Stack's functionality is on your `StackApp` object. Think of this object as the "connection" from your code to Stack's servers. Each app is always associated with one specific project ID (by default the one found in your environment variables). + +There is also a page on [StackApp](../sdk/objects/stack-app) in the SDK reference, which lists all available functions. + +## `getXyz`/`listXyz` vs. `useXyz` + +You will see that most of the asynchronous functions on `StackApp` come in two flavors: `getXyz`/`listXyz` and `useXyz`. The former are asynchronous fetching functions which return a `Promise`, while the latter are React hooks that [suspend](https://react.dev/reference/react/Suspense) the current component until the data is available. + +Normally, you would choose between the two based on whether you are in a React Server Component or a React Client Component. However, there are some scenarios where you use `getXyz` on the client, for example as the callback of an `onClick` handler. + +```tsx +// server-component.tsx +async function ServerComponent() { + const app = stackServerApp; + // returns a Promise, must be awaited + const user = await app.getUser(); + + return
{user.displayName}
; +} + + +// client-component.tsx +"use client"; +function ClientComponent() { + const app = useStackApp(); + // returns the value directly + const user = app.useUser(); + + return
{user.displayName}
; +} +``` + +## Client vs. server + +`StackClientApp` contains everything needed to build a frontend application, for example the currently authenticated user. It requires a publishable client key in its initialization (usually set by the `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY` environment variable). + +`StackServerApp` has all the functionality of `StackClientApp`, but also some functions with elevated permissions, eg. listing or modifying ALL users. This requires a secret server key (usually set by the `STACK_SECRET_SERVER_KEY` environment variable), which **must always be kept secret**. + +There is also a third type, `StackAdminApp`, but it is rarely used. You can use it for automation or internal tools, and can edit your project's configuration. + + + Some of the functions have different return types; for example, `StackClientApp.getUser()` returns a `Promise` while `StackServerApp.getUser()` returns a `Promise`. The `Server` or `Admin` prefixes indicate that the object contains server-/admin-only functionality. + diff --git a/docs/content/docs/(guides)/concepts/team-selection.mdx b/docs/content/docs/(guides)/concepts/team-selection.mdx new file mode 100644 index 0000000000..8a7bc760fd --- /dev/null +++ b/docs/content/docs/(guides)/concepts/team-selection.mdx @@ -0,0 +1,118 @@ +--- +title: Team Selection +--- + +A user can be a member of multiple teams, so most websites using teams will need a way to select a "current team" that the user is working on. There are two primary methods to accomplish this: + +- **Deep Link**: Each team has a unique URL, for example, `your-website.com/team/`. When a team is selected, it redirects to a page with that team's URL. +- **Current Team**: When a user selects a team, the app stores the team as a global "current team" state. In this way, the URL of the current team might be something like `your-website.com/current-team`, and the URL won't change after switching teams. + +## Deep Link Method +The deep link method is generally recommended because it avoids some common issues associated with the current team method. If two users share a link while using deep link URLs, the receiving user will always be directed to the correct team's information based on the link. + +## Current Team Method +While the current team method can be simpler to implement, it has a downside. If a user shares a link, the recipient might see information about the wrong team (if their "current team" is set differently). This method can also cause problems when a user has multiple browser tabs open with different teams. + +## Selected Team Switcher + +To facilitate team selection, Stack provides a component that looks like this: + +![TeamSwitcher](/imgs/team-switcher.png) + +You can import and use the `SelectedTeamSwitcher` component for the "current team" method. It updates the `selectedTeam` when a user selects a team: + +```jsx +import { SelectedTeamSwitcher } from "@stackframe/stack"; + +export function MyPage() { + return ( +
+ +
+ ); +} +``` + +To combine the switcher with the deep link method, you can pass in `urlMap` and `selectedTeam`. The `urlMap` is a function to generate a URL based on the team information, and `selectedTeam` is the team that the user is currently working on. This lets you implement "deep link" + "most recent team". The component will update the `user.selectedTeam` with the `selectedTeam` prop: + +```jsx + `/team/${team.id}`} + selectedTeam={team} +/> +``` + +To implement the "deep link" + "default team" method, where you update the `selectedTeam` only when the user clicks "set to default team" or similar, pass `noUpdateSelectedTeam`: + +```jsx + `/team/${team.id}`} + selectedTeam={team} + noUpdateSelectedTeam +/> +``` + +## Example: Deep Link + Most Recent Team + +First, create a page at `/app/team/[teamId]/page.tsx` to display information about a specific team: + +```jsx title="/app/team/[teamId]/page.tsx" +"use client"; + +import { useUser, SelectedTeamSwitcher } from "@stackframe/stack"; + +export default function TeamPage({ params }: { params: { teamId: string } }) { + const user = useUser({ or: 'redirect' }); + const team = user.useTeam(params.teamId); + + if (!team) { + return
Team not found
; + } + + return ( +
+ `/team/${team.id}`} + selectedTeam={team} + /> + +

Team Name: {team.displayName}

+

You are a member of this team.

+
+ ); +} +``` + +Next, create a page to display all teams at `/app/team/page.tsx`: + +```jsx title="/app/team/page.tsx" +"use client"; + +import { useRouter } from "next/navigation"; +import { useUser } from "@stackframe/stack"; + +export default function TeamsPage() { + const user = useUser({ or: 'redirect' }); + const teams = user.useTeams(); + const router = useRouter(); + const selectedTeam = user.selectedTeam; + + return ( +
+ {selectedTeam && + } + +

All Teams

+ {teams.map(team => ( + + ))} +
+ ); +} +``` + +Now, if you navigate to `http://localhost:3000/team`, you should be able to see and interact with the teams. diff --git a/docs/content/docs/(guides)/concepts/user-onboarding.mdx b/docs/content/docs/(guides)/concepts/user-onboarding.mdx new file mode 100644 index 0000000000..c451e1575f --- /dev/null +++ b/docs/content/docs/(guides)/concepts/user-onboarding.mdx @@ -0,0 +1,131 @@ +--- +title: User Onboarding +description: Implementing a user onboarding page and collecting information on sign-up +--- + +By default, Stack Auth collects information such as email addresses from OAuth providers. Sometimes, you may want to collect additional information from users during sign-up, for example a name or address. + +The most straightforward approach is to redirect users to an onboarding page right after they sign up. However, this is not recommended for the following reasons: + +1. Users can accidentally (or purposefully) close or navigate away from the page before completing the onboarding. +2. Redirect URLs may vary depending on the context. For instance, if a user is redirected to a sign-in page after trying to access a protected page, they'll expect to return to the original protected page post-authentication. + +Instead, a more reliable strategy is to store an `onboarded` flag in the user's metadata and redirect users to the onboarding page if they haven't completed it yet. + +## Example implementation + +Let's say you have an onboarding page that asks for an address and stores it in the user's [metadata](../concepts/custom-user-data.mdx): + +```jsx title="app/onboarding/page.tsx" +export default function OnboardingPage() { + const user = useUser(); + const router = useRouter(); + const [address, setAddress] = useState(''); + + + return <> + setAddress(e.target.value)} + /> + + + + ); +} +``` + + +While the above implementation offers a basic onboarding process, users can still skip onboarding by directly sending an API request to update the `clientMetadata.onboarded` flag. If you want to ensure that onboarding cannot be bypassed on the API level, you should create a server endpoint to validate and store the data, then save the `onboarded` flag in the `clientReadonlyMetadata` on the server side after validation. + + +Next, we can create a hook/function to check if the user has completed onboarding and redirect them to the onboarding page: + + + + Client Hook + Server Function + + + ```jsx title="app/onboarding-hooks.ts" + 'use client'; + import { useEffect } from 'react'; + import { useUser } from '@stackframe/stack'; + import { useRouter } from 'next/navigation'; + + export function useOnboarded() { + const user = useUser(); + const router = useRouter(); + + useEffect(() => { + if (!user.clientMetadata.onboarded) { + router.push('/onboarding'); + } + }, [user]); + } + ``` + + + ```jsx title="app/onboarding-functions.ts" + import { stackServerApp } from '@/stack/server'; + import { redirect } from 'next/navigation'; + + export async function ensureOnboarded() { + const user = await stackServerApp.getUser(); + if (!user.serverMetadata.onboarded) { + redirect('/onboarding'); + } + } + ``` + + + +You can then use these functions wherever onboarding is required: + + + + Client Component + Server Component + + + ```jsx title="app/page.tsx" + import { useOnboarding } from '@/app/onboarding-hooks'; + import { useUser } from '@stackframe/stack'; + + export default function HomePage() { + useOnboarding(); + const user = useUser(); + + return ( +
Welcome to the app, {user.displayName}
+ ); + } + ``` +
+ + ```jsx title="app/page.tsx" + import { ensureOnboarding } from '@/app/onboarding-functions'; + import { stackServerApp } from '@/stack/server'; + + export default async function HomePage() { + await ensureOnboarding(); + const user = await stackServerApp.getUser(); + + return ( +
Welcome to the app, {user.displayName}
+ ); + } + ``` +
+
diff --git a/docs/content/docs/(guides)/concepts/webhooks.mdx b/docs/content/docs/(guides)/concepts/webhooks.mdx new file mode 100644 index 0000000000..320cbfe474 --- /dev/null +++ b/docs/content/docs/(guides)/concepts/webhooks.mdx @@ -0,0 +1,69 @@ +--- +title: Webhooks +--- + +Webhooks are a powerful way to keep your backend in sync with Stack. They allow you to receive real-time updates when events occur in your Stack project, such as when a user or team is created, updated, or deleted. + +For more information and a list of all webhooks, please refer to the [webhook API reference](/api/webhooks/users/user.created). + +## Setting up webhooks + +In the Stack dashboard, you can create a webhook endpoint in the "Webhooks" section. After creating this endpoint with your server URL, you will start receiving POST requests with a JSON payload at that endpoint. The event payload will look something like this: + +```json +{ + "type": "team.created", + "data": { + "id": "2209422a-eef7-4668-967d-be79409972c5", + "display_name": "My Team", + ... + } +} +``` + +## Testing webhooks locally + +You can use services like [Svix Playground](https://www.svix.com/play/) or [Webhook.site](https://webhook.site/) to test the receiving of webhooks or relay them to your local development environment. + +## Verifying webhooks + +To ensure the webhook is coming from Stack (and not from a malicious actor) and is not prone to replay attacks, you should verify the request. + +Stack signs the webhook payload with a secret key that you can find in the endpoint details on the dashboard. You can verify the signature using the Svix client library. Check out the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how) for instructions on how to verify the signature in JavaScript, Python, Ruby, and other languages. Here is an quick example in JavaScript: + +```jsx +import { Webhook } from "svix"; + +const secret = ""; +const headers = { + "svix-id": "", + "svix-timestamp": "", + "svix-signature": "", +}; +const payload = ""; + +const wh = new Webhook(secret); +// Throws on error, returns the verified content on success +const payload = wh.verify(payload, headers); +``` + +If you do not want to install the Svix client library or are using a language that is not supported, you can [verify the signature manually](https://docs.svix.com/receiving/verifying-payloads/how-manual). + +## Event types + +Please refer to the webhook endpoint API reference for more details on the available event types and their payload structures. + +- [user.created](/api/webhooks/users/user.created) +- [user.updated](/api/webhooks/users/user.updated) +- [user.deleted](/api/webhooks/users/user.deleted) +- [team.created](/api/webhooks/teams/team.created) +- [team.updated](/api/webhooks/teams/team.updated) +- [team.deleted](/api/webhooks/teams/team.deleted) +- [team_membership.created](/api/webhooks/teams/team-membership.created) +- [team_membership.deleted](/api/webhooks/teams/team-membership.deleted) +- [team_permission.created](/api/webhooks/teams/team-permission.created) +- [team_permission.deleted](/api/webhooks/teams/team-permission.deleted) + +## Examples + +Some members of the community have shared their webhook implementations. For example, [here is an example by Clark Gredoña](https://gist.github.com/clarkg/56ffad44949826ae3efe0a431b6021c4) that validates the Webhook schema and update a database user. diff --git a/docs/content/docs/(guides)/customization/custom-pages.mdx b/docs/content/docs/(guides)/customization/custom-pages.mdx new file mode 100644 index 0000000000..c452aaadfe --- /dev/null +++ b/docs/content/docs/(guides)/customization/custom-pages.mdx @@ -0,0 +1,82 @@ +--- +title: Custom Pages +--- + +Custom pages allow you to take full control over the layout and logic flow of authentication pages in your application. Instead of using the default pages provided by Stack Auth, you can build your own using our built-in components or low-level functions. + +By default, `StackHandler` creates all authentication pages you need, however, you can replace them with your own custom implementations for a more tailored user experience. + +## Simple Example + +For example, if you want to create a custom sign-in page with a customized title on the top, you can create a file at `app/signin/page.tsx`: + +```tsx title="app/signin/page.tsx" +import { SignIn } from "@stackframe/stack"; + +export default function CustomSignInPage() { + return ( +
+

My Custom Sign In page

+ +
+ ); +} +``` + +Then you can instruct the Stack app in `stack/server.ts` to use your custom sign in page: + +```tsx title="stack/server.ts" +export const stackServerApp = new StackServerApp({ + // ... + // add these three lines + urls: { + signIn: '/signin', + } +}); +``` + +You are now all set! If you visit the `/signin` page, you should see your custom sign in page. When users attempt to access a protected page or navigate to the default `/handler/sign-in` URL, they will automatically be redirected to your new custom sign-in page. + +For more examples, please refer to the [Examples](../customization/custom-pages.mdx). + + +## Building From Scratch + +While the simple approach above lets you customize the layout while using Stack's pre-built components, sometimes you need complete control over both the UI and authentication logic. + +We also provide the low-level functions powering our components, so that you can build your own logic. For example, to build a custom OAuth sign-in button, create a file at `app/signin/page.tsx`: + +```tsx title="app/signin/page.tsx" +'use client'; +import { useStackApp } from "@stackframe/stack"; + +export default function CustomOAuthSignIn() { + const app = useStackApp(); + + return ( +
+

My Custom Sign In page

+ +
+ ); +} +``` + +Again, edit the Stack app in `stack/server.ts` to use your custom sign in page: + +```tsx title="stack/server.ts" +export const stackServerApp = new StackServerApp({ + // ... + // add these three lines + urls: { + signIn: '/signin', + } +}); +``` + +As above, visit the `/signin` page to see your newly created custom OAuth page. diff --git a/docs/content/docs/(guides)/customization/custom-styles.mdx b/docs/content/docs/(guides)/customization/custom-styles.mdx new file mode 100644 index 0000000000..afd99d58f0 --- /dev/null +++ b/docs/content/docs/(guides)/customization/custom-styles.mdx @@ -0,0 +1,53 @@ +--- +title: Custom Styles +--- + +Customizing the styles of your Stack Auth components allows you to maintain your brand identity while leveraging the pre-built functionality. This approach is ideal when you want to quickly align the authentication UI with your application's design system without building custom components from scratch. Stack's theming system uses a React context to store colors and styling variables that can be easily overridden. + +You can customize the following color variables to match your brand: + +- `background`: Main background color of the application +- `foreground`: Main text color on the background +- `card`: Background color for card elements +- `cardForeground`: Text color for card elements +- `popover`: Background color for popover elements like dropdowns +- `popoverForeground`: Text color for popover elements +- `primary`: Primary brand color, used for buttons and important elements +- `primaryForeground`: Text color on primary-colored elements +- `secondary`: Secondary color for less prominent elements +- `secondaryForeground`: Text color on secondary-colored elements +- `muted`: Color for muted or disabled elements +- `mutedForeground`: Text color for muted elements +- `accent`: Accent color for highlights and emphasis +- `accentForeground`: Text color on accent-colored elements +- `destructive`: Color for destructive actions like delete buttons +- `destructiveForeground`: Text color on destructive elements +- `border`: Color used for borders +- `input`: Border color for input fields +- `ring`: Focus ring color for interactive elements + +And some other variables: + +- `radius`: border radius of components like buttons, inputs, etc. + +These variables are CSS variables so you can use any valid CSS color syntax like `hsl(0, 0%, 0%)`, `black`, `#fff`, `rgb(255, 0, 0)`, etc. + +The colors can be different for light and dark mode, allowing you to create a cohesive experience across both themes. You can pass these into the `StackTheme` component (in your `layout.tsx` file if you followed the Getting Started guide) as follows: + +```jsx title="app/layout.tsx" +const theme = { + light: { + primary: 'red', + }, + dark: { + primary: '#00FF00', + }, + radius: '8px', +} + +// ... + + + {/* children */} + +``` diff --git a/docs/content/docs/(guides)/customization/dark-mode.mdx b/docs/content/docs/(guides)/customization/dark-mode.mdx new file mode 100644 index 0000000000..a0381abd76 --- /dev/null +++ b/docs/content/docs/(guides)/customization/dark-mode.mdx @@ -0,0 +1,61 @@ +--- +title: Dark Mode +--- + +Stack components support light and dark mode out of the box. All UI components automatically adapt their colors, shadows, and contrast levels based on the selected theme. + +You can switch between light and dark mode using [next-themes](https://github.com/pacocoursey/next-themes) (or any other library that changes the `data-theme` or `class` to `dark` or `light` attribute of the `html` element). + +Here is an example of how to set up next-themes with Stack (find more details in the [next-themes documentation](https://github.com/pacocoursey/next-themes)): + +1. Install next-themes: + + ```bash + npm install next-themes + ``` + +2. Add the `ThemeProvider` to your `layout.tsx` file: + + ```jsx + import { ThemeProvider } from 'next-themes' + + export default function Layout({ children }) { + return ( + {/* + ThemeProvider enables theme switching throughout the application. + defaultTheme="system" uses the user's system preference as the default. + attribute="class" applies the theme by changing the class on the html element. + */} + + {/* StackTheme ensures Stack components adapt to the current theme */} + + {children} + + + ) + } + ``` + +3. Build a color mode switcher component: + + ```jsx + 'use client'; + import { useTheme } from 'next-themes' + + export default function ColorModeSwitcher() { + // useTheme hook provides the current theme and a function to change it + const { theme, setTheme } = useTheme() + + return ( + + ) + } + ``` + +Now if you put the `ColorModeSwitcher` component in your app, you should be able to switch between light and dark mode. There should be no flickering or re-rendering of the page after reloading. diff --git a/docs/content/docs/(guides)/customization/internationalization.mdx b/docs/content/docs/(guides)/customization/internationalization.mdx new file mode 100644 index 0000000000..d27064cc15 --- /dev/null +++ b/docs/content/docs/(guides)/customization/internationalization.mdx @@ -0,0 +1,37 @@ +--- +title: Internationalization +--- + +Internationalization (i18n) allows your application to support multiple languages, making it accessible to users worldwide. Stack Auth provides built-in internationalization support for its components, enabling you to offer a localized authentication experience with minimal effort. + +## Setup + +Internationalization with Stack is very straightforward. Simply pass the `lang` prop to the `StackProvider` component, and all the pages will be translated to the specified language. + +```jsx title="layout.tsx" +... + + ... + +... +``` + +By default, if no language is provided, it will be set to `en-US`. + +You can choose which languages to use by employing your own methods, such as storing the language in `localStorage` or using the user's browser language. + +## Supported languages + +- `en-US`: English (United States) +- `de-DE`: German (Germany) +- `es-419`: Spanish (Latin America) +- `es-ES`: Spanish (Spain) +- `fr-CA`: French (Canada) +- `fr-FR`: French (France) +- `it-IT`: Italian (Italy) +- `pt-BR`: Portuguese (Brazil) +- `pt-PT`: Portuguese (Portugal) +- `zh-CN`: Chinese (China) +- `zh-TW`: Chinese (Taiwan) +- `ja-JP`: Japanese (Japan) +- `ko-KR`: Korean (South Korea) diff --git a/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx b/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx new file mode 100644 index 0000000000..7da437e827 --- /dev/null +++ b/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx @@ -0,0 +1,97 @@ +--- +title: Forgot Password +--- + +This page provides examples of how to create custom "forgot password" pages for your application. The forgot password functionality allows users to request a password reset email when they can't remember their current password. + +## Custom page with `ForgotPassword` component + +The `ForgotPassword` component provides a complete form for users to request a password reset email. When a user submits their email, Stack Auth will send them an email with a link to reset their password. + +```tsx +'use client'; +import { ForgotPassword } from "@stackframe/stack"; + +export default function DefaultForgotPassword() { + return ; +} +``` + +## Integration with Application Routing + +To integrate the forgot password page with your application's routing: + +1. Create a route for your forgot password page (e.g., `/forgot-password`) +2. Configure Stack Auth to use your custom route in your `stack/server.ts` file: + +```tsx +export const stackServerApp = new StackServerApp({ + // ... + urls: { + forgotPassword: '/forgot-password', + } +}); +``` + +This ensures that links to the forgot password page will direct users to your custom implementation. When a user submits their email, Stack Auth will send them an email with a link to the password reset page configured in your application. + + +## Custom forgot password form + +If you need more control over the forgot password process, you can build your own form. This approach allows you to customize the UI and error handling to match your application's design. + +```tsx +'use client'; + +import { useStackApp } from "@stackframe/stack"; +import { useState } from "react"; + +export default function CustomForgotPasswordForm() { + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [message, setMessage] = useState(''); + const app = useStackApp(); + + const onSubmit = async () => { + if (!email) { + setError('Please enter your email address'); + return; + } + + try { + const result = await app.sendForgotPasswordEmail(email); + if (result?.status === 'error') { + if (result.error.code === 'user_not_found') { + // For security reasons, don't reveal if a user exists or not + setMessage('If an account exists with this email, a password reset link has been sent.'); + } else { + setError(`Error: ${result.error.message}`); + } + } else { + setMessage('Password reset email sent! Please check your inbox.'); + } + } catch (err) { + setError(`An unexpected error occurred: ${err.message}`); + } + }; + + return ( +
{ e.preventDefault(); onSubmit(); }}> + {error &&
{error}
} + {message ? ( +
{message}
+ ) : ( + <> + setEmail(e.target.value)} + /> + + + )} +
+ ); +} +``` diff --git a/docs/content/docs/(guides)/customization/page-examples/index.mdx b/docs/content/docs/(guides)/customization/page-examples/index.mdx new file mode 100644 index 0000000000..6987b6fef6 --- /dev/null +++ b/docs/content/docs/(guides)/customization/page-examples/index.mdx @@ -0,0 +1,15 @@ +--- +title: Page Examples +--- + + +This section contains examples of how to customize various authentication pages in your Stack Auth application. + +Browse the examples to learn how to: + +- Create custom sign-in pages +- Build custom sign-up forms +- Implement password reset flows +- Handle forgotten password scenarios + +Each example shows both the basic component usage and advanced customization techniques. diff --git a/docs/content/docs/(guides)/customization/page-examples/meta.json b/docs/content/docs/(guides)/customization/page-examples/meta.json new file mode 100644 index 0000000000..8ab1142a25 --- /dev/null +++ b/docs/content/docs/(guides)/customization/page-examples/meta.json @@ -0,0 +1,10 @@ +{ + "title": "Page Examples", + "defaultOpen": false, + "pages": [ + "forgot-password", + "password-reset", + "sign-in", + "sign-up" + ] +} diff --git a/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx b/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx new file mode 100644 index 0000000000..e10681337d --- /dev/null +++ b/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx @@ -0,0 +1,106 @@ +--- +title: Password Reset +--- + +This page provides examples of how to create custom password reset pages for your application. Password reset functionality allows users to securely create a new password when they've forgotten their current one. + +## Custom page with `PasswordReset` component + +The `PasswordReset` component provides a complete password reset form with built-in validation and error handling. This is the simplest way to add password reset functionality to your application. + +```tsx +'use client'; +import { PasswordReset } from "@stackframe/stack"; + +export default function DefaultPasswordReset() { + return ; +} +``` + +## Integration with Application Routing + +To integrate the password reset page with your application's routing: + +1. Create a route handler that extracts the reset code from the URL (e.g., `/reset-password?code=xyz123`) +2. Pass the code to your password reset component +3. Configure Stack Auth to use your custom route in your `stack/server.ts` file: + +```tsx +export const stackServerApp = new StackServerApp({ + // ... + urls: { + passwordReset: '/reset-password', + } +}); +``` + +This ensures that password reset links in emails will direct users to your custom page. + + +## Custom password reset form + +If you need more control over the password reset process, you can build your own form using the Stack Auth API. This approach allows you to customize the UI and error handling to match your application's design. + +The `code` parameter used below is typically extracted from the URL query parameters. This code is sent to the user's email when they request a password reset and is required to validate the reset request. + +```tsx +'use client'; + +import { useStackApp } from "@stackframe/stack"; +import { useState } from "react"; + +export default function CustomPasswordResetForm({ code }: { code: string }) { + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const app = useStackApp(); + + const onSubmit = async () => { + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + try { + const result = await app.resetPassword({ password, code }); + if (result.status === 'error') { + setError('Failed to reset password'); + return; + } + setSuccess(true); + } catch (err) { + setError(`An unexpected error occurred: ${err.message}`); + } + }; + + if (success) { + return
Password successfully reset!
; + } + + return ( +
{ e.preventDefault(); onSubmit(); }}> + {error &&
{error}
} +
+ + setPassword(e.target.value)} + /> +
+
+ + setConfirmPassword(e.target.value)} + /> +
+ +
+ ); +} +``` diff --git a/docs/content/docs/(guides)/customization/page-examples/sign-in.mdx b/docs/content/docs/(guides)/customization/page-examples/sign-in.mdx new file mode 100644 index 0000000000..ec96ae55a9 --- /dev/null +++ b/docs/content/docs/(guides)/customization/page-examples/sign-in.mdx @@ -0,0 +1,141 @@ +--- +title: Sign-In Page Examples +--- + +# Sign-In Page Examples + +This page provides examples of how to create custom sign-in pages for your application using Stack Auth components and functions. + +## Custom page with `SignIn` component + +```tsx +'use client'; +import { SignIn } from "@stackframe/stack"; + +export default function DefaultSignIn() { + // optionally redirect to some other page if the user is already signed in + // const user = useUser(); + // if (user) { redirect to some other page } + return ; +} +``` + +You can also use `useUser` at the beginning of the sign-in page to check whether the user is already signed in and redirect them to another page if they are. + + +## Other useful components + +`CredentialSignIn`: A component that renders a complete form for signing in with email and password. It handles validation, error states, and submission automatically. + +`OAuthGroup`: A component that displays a list of available OAuth provider sign-in buttons. The available provider list is automatically fetched from the server based on your project configuration. + +`OAuthButton`: A component that renders a single OAuth sign-in button for a specific provider. Use this when you only want to offer specific OAuth providers. + + +## Custom OAuth Sign In + +```tsx +'use client'; +import { useStackApp } from "@stackframe/stack"; + +export default function CustomOAuthSignIn() { + const app = useStackApp(); + + return ( +
+

My Custom Sign In page

+ +
+ ); +} +``` + +## Custom Credential Sign In + +```tsx +'use client'; +import { useStackApp } from "@stackframe/stack"; +import { useState } from "react"; + +export default function CustomCredentialSignIn() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const app = useStackApp(); + + const onSubmit = async () => { + if (!password) { + setError('Please enter your password'); + return; + } + // This will redirect to app.urls.afterSignIn if successful. + // You can customize the redirect URL in the StackServerApp constructor. + const result = await app.signInWithCredential({ email, password }); + // It's better to handle each error code separately, but for simplicity, + // we'll just display the error message directly here. + if (result.status === 'error') { + setError(result.error.message); + } + }; + + return ( +
{ e.preventDefault(); onSubmit(); } }> + {error} + setEmail(e.target.value)} /> + setPassword(e.target.value)} /> + +
+ ); +} +``` + +## Custom Magic Link Sign In + +```tsx +'use client'; + +import { useStackApp } from "@stackframe/stack"; +import { useState } from "react"; + +export default function CustomMagicLinkSignIn() { + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [message, setMessage] = useState(''); + const app = useStackApp(); + + const onSubmit = async () => { + if (!email) { + setError('Please enter your email address'); + return; + } + + // This will send a magic link email to the user's email address. + // When they click the link, they will be redirected to your application. + const result = await app.sendMagicLinkEmail(email); + // It's better to handle each error code separately, but for simplicity, + // we'll just display the error message directly here. + if (result.status === 'error') { + setError(result.error.message); + } else { + setMessage('Magic link sent! Please check your email.'); + } + }; + + return ( +
{ e.preventDefault(); onSubmit(); } }> + {error} + {message ? +
{message}
: + <> + setEmail(e.target.value)} /> + + } +
+ ); +} +``` diff --git a/docs/content/docs/(guides)/customization/page-examples/sign-up.mdx b/docs/content/docs/(guides)/customization/page-examples/sign-up.mdx new file mode 100644 index 0000000000..72f0b1ad55 --- /dev/null +++ b/docs/content/docs/(guides)/customization/page-examples/sign-up.mdx @@ -0,0 +1,79 @@ +--- +title: Sign-Up Page Examples +--- + +# Custom Sign-Up Page Examples + +This page provides examples of how to create custom sign-up pages for your application using Stack Auth components and functions. + +## Custom page with `SignUp` component + +```tsx +'use client'; +import { SignUp } from "@stackframe/stack"; + +export default function DefaultSignUp() { + // optionally redirect to some other page if the user is already signed in + // const user = useUser(); + // if (user) { redirect to some other page } + return ; +} +``` + +You can also use `useUser` at the beginning of the sign-up page to check whether the user is already signed in and redirect them to another page if they are. + +## Other useful components + +`CredentialSignUp`: A component that renders a complete form for signing up with email and password. It handles validation, error states, and submission automatically. + +`OAuthGroup`: A component that displays a list of available OAuth provider sign-up buttons. The available provider list is automatically fetched from the server based on your project configuration. + +`OAuthButton`: A component that renders a single OAuth sign-up button for a specific provider. Use this when you only want to offer specific OAuth providers. + +## Custom OAuth Sign Up + +OAuth sign-in and sign-up share the same function. Check out the [Sign In example](/customization/page-examples/sign-in#custom-oauth-sign-in) for more information. + +## Custom Credential Sign Up + +```tsx +'use client'; + +import { useStackApp } from "@stackframe/stack"; +import { useState } from "react"; + +export default function CustomCredentialSignUp() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const app = useStackApp(); + + const onSubmit = async () => { + if (!password) { + setError('Please enter your password'); + return; + } + // This will redirect to app.urls.afterSignUp if successful. + // You can customize the redirect URL in the StackServerApp constructor. + const result = await app.signUpWithCredential({ email, password }); + // It's better to handle each error code separately, but for simplicity, + // we'll just display the error message directly here. + if (result.status === 'error') { + setError(result.error.message); + } + }; + + return ( +
{ e.preventDefault(); onSubmit(); } }> + {error} + setEmail(e.target.value)} /> + setPassword(e.target.value)} /> + +
+ ); +} +``` + +## Custom Magic Link Sign Up + +Magic link sign-in and sign-up shares the same function. Check out the [Sign In example](/customization/page-examples/signin#custom-magic-link-sign-in) for more information. diff --git a/docs/content/docs/(guides)/faq.mdx b/docs/content/docs/(guides)/faq.mdx new file mode 100644 index 0000000000..8f43a94a52 --- /dev/null +++ b/docs/content/docs/(guides)/faq.mdx @@ -0,0 +1,40 @@ +--- +title: FAQ +description: Frequently asked questions about Stack +--- + +## Languages & Frameworks + + + For frontends, Stack supports TypeScript and JavaScript. For backends, Stack has a flexible [REST API](/rest-api) that can be used with any language or framework. + + + Yes! You can use our vanilla JavaScript SDK, or, if the framework is React-based, our React SDK. + + + Only the Next.js app router is currently officially supported, although some members of the community have successfully used the React or vanilla JavaScript SDKs with the pages router. + + + +## Product + + + Ask yourself about ``: + + - Is `` open-source? + - Is `` developer-friendly, well-documented, and lets you get started in minutes? + - Besides authentication, does `` also do authorization and user management (see feature list below)? + + If you answered "no" to any of these questions, then that's how Stack Auth is different from ``. + + + Yes! You can [create users programmatically](/rest-api/server/users/create-user) using our [REST API](/rest-api). + + + +## Other + + + Please carefully read our [CONTRIBUTING.md](https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md). + + diff --git a/docs/content/docs/(guides)/getting-started/components.mdx b/docs/content/docs/(guides)/getting-started/components.mdx new file mode 100644 index 0000000000..8fdd3c3c36 --- /dev/null +++ b/docs/content/docs/(guides)/getting-started/components.mdx @@ -0,0 +1,62 @@ +--- +title: Components +description: Pre-built Next.js components to make your life easier +--- + +In [the last guide](./setup.mdx), we initialized Stack. This time, we will take a quick look at some of the most useful Next.js components. + +For the full documentation of all available components, please refer to the [components reference](/components). + +## `` + +The `` component shows the user's avatar that opens a dropdown with various user settings on click. + +
+ UserButton +
+ +```tsx title="page.tsx" +import { UserButton } from '@stackframe/stack'; + +export default function Page() { + return ( + + ); +} +``` + +## `` and `` + +These components show a sign-in and sign-up form, respectively. + +
+ SignIn +
+ +```tsx title="page.tsx" +import { SignIn } from '@stackframe/stack'; + +export default function Page() { + return ( + + ); +} +``` + +All of Stack's components are modular and built from smaller primitives. For example, the `` component is composed of the following: + +- An ``, which itself is composed of multiple `` components +- A ``, which has a text field and calls `useStackApp().signInWithMagicLink()` +- A ``, which has two text fields and calls `useStackApp().signInWithCredential()` + +You can use these components individually to build a custom sign-in component. + +To change the default sign-in URL to your own, see the documentation on [custom pages](../customization/custom-pages.mdx). + +## Others + +Stack has many more components available. For a comprehensive list, please check the documentation on [components](../components/overview). + +## Next steps + +In the next guide, we will do a deep-dive into retrieving and modifying user objects, as well as how to protect a page. diff --git a/docs/content/docs/(guides)/getting-started/example-pages.mdx b/docs/content/docs/(guides)/getting-started/example-pages.mdx new file mode 100644 index 0000000000..913db8b005 --- /dev/null +++ b/docs/content/docs/(guides)/getting-started/example-pages.mdx @@ -0,0 +1,455 @@ +--- +title: Example-pages +--- + + +This guide demonstrates how to integrate Stack Auth with Vite. The same principles apply to other JavaScript frameworks as well. You can find the complete example code in our [GitHub repository](https://github.com/stack-auth/stack-auth/tree/main/examples/js-example). + +### Initialize the app + +```typescript title="stack/client.ts" +import { StackClientApp } from "@stackframe/js"; + +// Add type declaration for Vite's import.meta.env +declare global { + interface ImportMeta { + env: { + VITE_STACK_API_URL: string; + VITE_STACK_PROJECT_ID: string; + VITE_STACK_PUBLISHABLE_CLIENT_KEY: string; + }; + } +} + +export const stackClientApp = new StackClientApp({ + baseUrl: import.meta.env.VITE_STACK_API_URL, + projectId: import.meta.env.VITE_STACK_PROJECT_ID, + publishableClientKey: import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY, + tokenStore: "cookie", + urls: { + oauthCallback: window.location.origin + "/oauth", + }, +}); +``` + +### Index page with user information + + + + index.html + index-script.ts + + + + ```html + + + + + + Stack Auth JS Examples + + +

Stack Auth JS Examples

+ + + + + + + + + ``` +
+ + + ```typescript + import { stackClientApp } from "./stack/client"; + + const updateUIState = (user: any | null) => { + const authOptions = document.getElementById("authOptions"); + const userInfo = document.getElementById("userInfo"); + const userEmailSpan = document.getElementById("userEmail"); + + if (user) { + if (authOptions) authOptions.style.display = "none"; + if (userInfo) userInfo.style.display = "block"; + if (userEmailSpan) userEmailSpan.textContent = user.primaryEmail || ""; + } else { + if (authOptions) authOptions.style.display = "block"; + if (userInfo) userInfo.style.display = "none"; + } + }; + + // Check if user is already signed in + stackClientApp.getUser().then(updateUIState); + + // Handle Sign Out + document.getElementById("signOut")?.addEventListener("click", async () => { + const user = await stackClientApp.getUser(); + if (user) { + await user.signOut(); + updateUIState(null); + } + }); + ``` + +
+ + +### Sign in with password + + + + password-sign-in.html + password-sign-in-script.ts + + + + ```html + + + + + + Password Sign In + + +

Password Sign In

+

← Back to home

+ +
+

Sign In

+ + + +
+

Don't have an account? Create account

+
+
+ + + + + ``` +
+ + + ```typescript + import { stackClientApp } from "./stack/client"; + + // Check if user is already signed in + stackClientApp.getUser().then((user) => { + if (user) { + window.location.href = "/"; + } + }); + + document.getElementById("showSignUp")?.addEventListener("click", (e) => { + e.preventDefault(); + document.getElementById("loginForm")?.classList.add("hidden"); + document.getElementById("signUpForm")?.classList.remove("hidden"); + }); + + document.getElementById("showSignIn")?.addEventListener("click", (e) => { + e.preventDefault(); + document.getElementById("loginForm")?.classList.remove("hidden"); + document.getElementById("signUpForm")?.classList.add("hidden"); + }); + + document.getElementById("signIn")?.addEventListener("click", async () => { + const emailInput = document.getElementById("emailInput") as HTMLInputElement; + const passwordInput = document.getElementById("passwordInput") as HTMLInputElement; + + const result = await stackClientApp.signInWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (result.status === "error") { + alert("Sign in failed. Please check your email and password and try again."); + } else { + window.location.href = "/"; + } + }); + + document.getElementById("signUp")?.addEventListener("click", async () => { + const emailInput = document.getElementById("signUpEmail") as HTMLInputElement; + const passwordInput = document.getElementById("signUpPassword") as HTMLInputElement; + + const result = await stackClientApp.signUpWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (result.status === "error") { + alert("Sign up failed. Please try again."); + return; + } + + const signInResult = await stackClientApp.signInWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (signInResult.status === "error") { + alert("Account created but sign in failed. Please sign in manually."); + } else { + window.location.href = "/"; + } + }); + ``` + +
+ +### Sign up with password + + + + password-sign-up.html + password-sign-up-script.ts + + + + ```html + + + + + + Password Sign Up + + +

Password Sign Up

+

← Back to home

+ +
+

Sign Up

+ + + +
+

Already have an account? Sign in

+
+
+ + + + + ``` +
+ + + ```typescript + import { stackClientApp } from "./stack/client"; + + // Check if user is already signed in + stackClientApp.getUser().then((user) => { + if (user) { + window.location.href = "/"; + } + }); + + document.getElementById("signUp")?.addEventListener("click", async () => { + const emailInput = document.getElementById("signUpEmail") as HTMLInputElement; + const passwordInput = document.getElementById("signUpPassword") as HTMLInputElement; + + const result = await stackClientApp.signUpWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (result.status === "error") { + alert("Sign up failed. Please try again."); + return; + } + + const signInResult = await stackClientApp.signInWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (signInResult.status === "error") { + alert("Account created but sign in failed. Please sign in manually."); + window.location.href = "/password-sign-in"; + } else { + window.location.href = "/"; + } + }); + ``` + +
+ +### Sign in with OTP/Magic Link + + + + otp-sign-in.html + otp-sign-in-script.ts + + + + ```html + + + + + + OTP Sign In + + +

OTP Sign In

+

← Back to home

+ +
+

Sign In with Email Code

+
+ + +
+ + +
+ + + + + ``` +
+ + + ```typescript + import { stackClientApp } from "./stack/client"; + + // Check if user is already signed in + stackClientApp.getUser().then((user) => { + if (user) { + window.location.href = "/"; + } + }); + + document.getElementById("signUp")?.addEventListener("click", async () => { + const emailInput = document.getElementById("signUpEmail") as HTMLInputElement; + const passwordInput = document.getElementById("signUpPassword") as HTMLInputElement; + + const result = await stackClientApp.signUpWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (result.status === "error") { + alert("Sign up failed. Please try again."); + return; + } + + const signInResult = await stackClientApp.signInWithCredential({ + email: emailInput.value, + password: passwordInput.value, + }); + + if (signInResult.status === "error") { + alert("Account created but sign in failed. Please sign in manually."); + window.location.href = "/password-sign-in"; + } else { + window.location.href = "/"; + } + }); + ``` + +
+ +### OAuth sign in + + + + oauth.html + oauth-script.ts + + + + ```html + + + + + + OAuth Authentication + + + +

OAuth Authentication

+

← Back to home

+ +
+

Sign In with OAuth

+ +
+ + + + + ``` +
+ + + ```typescript + import { stackClientApp } from "./stack/client"; + + // Check if user is already signed in + stackClientApp.getUser().then((user) => { + if (user) { + window.location.href = "/"; + } + }); + + // Handle Google Sign In + document.getElementById("googleSignIn")?.addEventListener("click", async () => { + try { + await stackClientApp.signInWithOAuth('google'); + } catch (error) { + console.error("Google sign in failed:", error); + alert("Failed to initialize Google sign in"); + } + }); + + // Handle OAuth redirect + window.addEventListener("load", async () => { + try { + const params = new URLSearchParams(window.location.search); + const code = params.get("code"); + const state = params.get("state"); + + if (code && state) { + const user = await stackClientApp.callOAuthCallback(); + if (user) { + window.location.href = "/"; + } + } + } catch (error) { + console.error("Failed to handle OAuth redirect:", error); + alert("Authentication failed. Please try again."); + } + }); + ``` + +
diff --git a/docs/content/docs/(guides)/getting-started/production.mdx b/docs/content/docs/(guides)/getting-started/production.mdx new file mode 100644 index 0000000000..2f1365f2e1 --- /dev/null +++ b/docs/content/docs/(guides)/getting-started/production.mdx @@ -0,0 +1,148 @@ +--- +title: Production +description: Steps to prepare Stack for production use +--- + +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: + +### 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. + +Follow these steps when you're ready to push your application to production: + + + + ## Add Your Domain + + 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/stack-app.mdx)). + + + ## Disable Localhost Callbacks + + For enhanced security, disable the `Allow all localhost callbacks for development` option. + + +### 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. + +To use your own OAuth provider setups in production, follow these steps for each provider you use: + + + + ## Create an OAuth App + + 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. + + + + Google + GitHub + Facebook + Microsoft + Spotify + Gitlab + Bitbucket + LinkedIn + X + + + + [Google OAuth Setup Guide](https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name-.) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/google + ``` + + + + [GitHub OAuth Setup Guide](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/github + ``` + + + + [Facebook OAuth Setup Guide](https://developers.facebook.com/docs/development/create-an-app/facebook-login-use-case) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/facebook + ``` + + + + [Microsoft Azure OAuth Setup Guide](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/microsoft + ``` + + + + [Spotify OAuth Setup Guide](https://developer.spotify.com/documentation/general/guides/app-settings/) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/spotify + ``` + + + + [Gitlab OAuth Setup Guide](https://docs.gitlab.com/ee/integration/oauth_provider.html) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/gitlab + ``` + + + + [Bitbucket OAuth Setup Guide](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/bitbucket + ``` + + + + [LinkedIn OAuth Setup Guide](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS1) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/linkedin + ``` + + + + [X OAuth Setup Guide](https://developer.x.com/en/docs/apps/overview) + + Callback URL: + ``` + https://api.stack-auth.com/api/v1/auth/oauth/callback/x + ``` + + + + ## Enter OAuth Credentials + + 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. + + +### 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. + +Steps to connect your own email server with Stack: +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. + +### 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. diff --git a/docs/content/docs/(guides)/getting-started/setup.mdx b/docs/content/docs/(guides)/getting-started/setup.mdx new file mode 100644 index 0000000000..2c6c838450 --- /dev/null +++ b/docs/content/docs/(guides)/getting-started/setup.mdx @@ -0,0 +1,595 @@ +--- +title: Setup +--- + +{/* IF_PLATFORM: next */} + + +Welcome to the Next.js SDK setup guide. If you're looking for guides for other frameworks, check out the [React SDK Setup](/docs/react/getting-started/setup), or the [JavaScript SDK Setup](/docs/js/getting-started/setup). + + +## Setup + +Before getting started, make sure you have a [Next.js project](https://nextjs.org/docs/getting-started/installation) using the app router, as Stack Auth does not support the pages router. + +We recommend using our **setup wizard** for a seamless installation experience. The wizard automatically detects your project structure and walks you through the setup process. If you encounter any issues with the wizard, you can follow our manual installation steps instead. + + + + Setup wizard (recommended) + Manual installation + + + + + + ### Run installation wizard + + Run Stack's installation wizard with the following command: + + ```sh title="Terminal" + npx @stackframe/init-stack@latest + ``` + + + ### Update API keys + + Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its environment variables into the `.env.local` file of your Next.js project: + + ```sh title=".env.local" + NEXT_PUBLIC_STACK_PROJECT_ID= + NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= + STACK_SECRET_SERVER_KEY= + ``` + + + ### Done! + + That's it! The following files should have been created or updated in your project: + + - `app/handler/[...stack]/page.tsx`: This file contains the default pages for sign-in, sign-out, account settings, and more. If you prefer, later you will learn how to [use custom pages](../customization/custom-pages.mdx) instead. + - `app/layout.tsx`: The layout file was updated to wrap the entire body with `StackProvider` and `StackTheme`. + - `app/loading.tsx`: If not yet found, Stack automatically adds a Suspense boundary to your app. This is shown to the user while Stack's async hooks, like `useUser`, are loading. + - `stack/server.ts`: This file contains the `stackServerApp` which you can use to access Stack from Server Components, Server Actions, API routes, and middleware. + - `stack/client.ts`: This file contains the `stackClientApp` which you can use to access Stack from Client Components + + + + + + Note: The setup wizard also supports existing, complicated projects. Cases where manual installation is necessary are rare. + + + If you are struggling with the setup wizard, please reach out to us on our [Discord](https://discord.stack-auth.com) first, where we'll be happy to help you. + + + ### Install npm package + + First, install Stack with npm, yarn, or pnpm: + + ```bash title="Terminal" + npm install @stackframe/stack + ``` + + + ### Create API keys + + If you haven't already, [register a new account on Stack](https://app.stack-auth.com/handler/sign-up). Create a project in the dashboard, create a new API key from the left sidebar, and copy the project ID, publishable client key, and secret server key into a new file called `.env.local` in the root of your Next.js project: + + ```sh title=".env.local" + NEXT_PUBLIC_STACK_PROJECT_ID= + NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= + STACK_SECRET_SERVER_KEY= + ``` + + + ### Create `stack/server.ts` file + + Create a new file `stack/server.ts` in your root directory and fill it with the following: + + ```tsx title="stack/server.ts" + import "server-only"; + import { StackServerApp } from "@stackframe/stack"; + + export const stackServerApp = new StackServerApp({ + tokenStore: "nextjs-cookie", // storing auth tokens in cookies + }); + ``` + + This will read the environment variables automatically and create a server app that you can later use to access Stack from your Next.js server. + + Check out the [`StackServerApp` documentation](../sdk/objects/stack-app.mdx) to learn more about its other options. + + + ### Create Stack handler + + Create a new file in `app/handler/[...stack]/page.tsx` and paste the following code: + + ```tsx title="app/handler/[...stack]/page.tsx" + import { StackHandler } from "@stackframe/stack"; + import { stackServerApp } from "@/stack/server"; + + export default function Handler(props: unknown) { + return ; + } + ``` + + This will create pages for sign-in, sign-up, password reset, and others. Additionally, it will be used as a callback URL for OAuth. You can [replace them with your own pages](../customization/custom-pages.mdx) later. + + + ### Add StackProvider to `layout.tsx` + + In your `app/layout.tsx`, wrap the entire body with a `StackProvider` and `StackTheme`. Afterwards, it should look like this: + + ```tsx title="app/layout.tsx" + import React from "react"; + import { StackProvider, StackTheme } from "@stackframe/stack"; + import { stackServerApp } from "@/stack/server"; + + export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + {children} + + + + + ); + } + ``` + + + ### Add Suspense boundary + + By default, Stack uses [`Suspense`](https://react.dev/reference/react/Suspense) to handle loading states. To show a loading indicator while Stack is fetching user data, make sure there is a `loading.tsx` file in your `app` directory: + + ```tsx title="app/loading.tsx" + export default function Loading() { + // You can use any loading indicator here + return <> + Loading... + ; + } + ``` + + + ### Done! + + + + + +## Post-setup + +That's it! Stack is now configured in your Next.js project. If you start your Next.js app with `npm run dev` and navigate to [http://localhost:3000/handler/signup](http://localhost:3000/handler/sign-up), you will see the sign-up page. + +
+ SignIn +
+ +After signing up/in, you will be redirected back to the home page. We will show you how to add user information to it in the next section. You can also check out the [http://localhost:3000/handler/account-settings](http://localhost:3000/handler/account-settings) page which looks like this: + +![Stack account settings page](/imgs/account-settings.png) + + +## Next steps + +Next up, we will show you how to [retrieve and update user information](./users.mdx), and how to [protect a page](./users.mdx#protecting-a-page) from unauthorized access. + +{/* ELSE_IF_PLATFORM react */} + + +Welcome to the React SDK setup guide! If you're looking for guides for other frameworks, check out the [Next.js SDK Setup](/docs/next/getting-started/setup), or the [JavaScript SDK Setup](/docs/js/getting-started/setup). + + +Before getting started, make sure you have a [React project](https://react.dev/learn/creating-a-react-app) setup. We show an example here of a Vite React project. + +We recommend using our **setup wizard** for a seamless installation experience. The wizard automatically detects your project structure and walks you through the setup process. If you encounter any issues with the wizard, you can follow our manual installation steps instead. + + + + Setup wizard (recommended) + Manual installation + + + + + + ### Run installation wizard + + Run Stack's installation wizard with the following command: + + ```sh title="Terminal" + npx @stackframe/init-stack@latest + ``` + + + ### Update API keys + + Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its values into the `stack/client.ts` file created by the wizard. + + + ### Wrap your app + + This example uses react-router, but all React apps should wrap the app with `StackProvider` and `StackTheme`. + + ```tsx title="App.tsx" + import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; + import { Suspense } from "react"; + import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; + import { stackClientApp } from "./stack/client"; + + function HandlerRoutes() { + const location = useLocation(); + return ( + + ); + } + + export default function App() { + return ( + + + + + + } /> + hello world
} /> + + + + + + ); + } + ``` + + + ### Done! + + That's it! Stack is now configured in your React project. If you start your app and navigate to [http://localhost:5173/handler/sign-up](http://localhost:5173/handler/sign-up), you will see the sign-up page. + +
+ SignIn +
+ + After signing up/in, you will be redirected back to the home page. You can also check out the [http://localhost:5173/handler/account-settings](http://localhost:5173/handler/account-settings) page. + + + + + + + ### Install npm package + + + ```bash title="Terminal" + npm install @stackframe/react + ``` + + + ### Create API keys + + If you haven't already, [register a new account on Stack](https://app.stack-auth.com/projects), create a project in the dashboard, create a new API key from the left sidebar, and copy the project ID and publishable client key. Store them based on your project setup (environment variables or directly in the client file during development). + + + ### Create `stack/client.ts` file + + Create a new file `stack/client.ts` in your root directory and fill it with the following Stack app initialization code: + + ```tsx title="stack/client.ts" + import { StackClientApp } from "@stackframe/react"; + // If you use React Router, uncomment the next line and the redirectMethod below + // import { useNavigate } from "react-router-dom"; + + export const stackClientApp = new StackClientApp({ + // You should store these in environment variables based on your project setup + projectId: "your-project-id", + publishableClientKey: "your-publishable-client-key", + tokenStore: "cookie", + // redirectMethod: { useNavigate }, // Optional: only if using react-router-dom + }); + ``` + + + ### Update `App.tsx` + + If you're using React Router, update your `App.tsx` file to wrap the entire app with a `StackProvider` and `StackTheme` and add a `StackHandler` route to handle the authentication flow. + + ```tsx title="App.tsx" + import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; + import { Suspense } from "react"; + import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; + import { stackClientApp } from "./stack/client"; + + function HandlerRoutes() { + const location = useLocation(); + return ( + + ); + } + + export default function App() { + return ( + + + + + + } /> + hello world
} /> + + + + + + ); + } + ``` + + + ### Done! + + That's it! Stack is now configured in your React project. If you start your app and navigate to [http://localhost:5173/handler/sign-up](http://localhost:5173/handler/sign-up), you will see the sign-up page. + +
+ SignIn +
+ + After signing up/in, you will be redirected back to the home page. You can also check out the [http://localhost:5173/handler/account-settings](http://localhost:5173/handler/account-settings) page. + + + + + +{/* ELSE_IF_PLATFORM js */} + + +Welcome to the JavaScript SDK setup guide. If you're looking for guides for other frameworks, check out the [React SDK Setup](/docs/react/getting-started/setup), or the [Next.js SDK Setup](/docs/next/getting-started/setup). + + +Before getting started, make sure you have a JavaScript project set up (such as Node.js, Vite, or any other JavaScript framework). + +We recommend using our **setup wizard** for a seamless installation experience. The wizard automatically detects your project structure and walks you through the setup process. If you encounter any issues with the wizard, you can follow our manual installation steps instead. + + + + Setup wizard (recommended) + Manual installation + + + + + + ### Run installation wizard + + Run Stack's installation wizard with the following command: + + ```sh title="Terminal" + npx @stackframe/init-stack@latest + ``` + + + ### Update API keys + + Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its values into the `stack/server.ts` or `stack/client.ts` file. + + + + Server + Client + + + + ```tsx title="stack/server.ts" + import { StackServerApp } from "@stackframe/js"; + + export const stackServerApp = new StackServerApp({ + // You should store these in environment variables based on your project setup + projectId: "your-project-id", + publishableClientKey: "your-publishable-client-key", + secretServerKey: "your-secret-server-key", + tokenStore: "memory", + }); + ``` + + + + ```tsx title="stack/client.ts" + import { StackClientApp } from "@stackframe/js"; + + export const stackClientApp = new StackClientApp({ + // You should store these in environment variables based on your project setup + projectId: "your-project-id", + publishableClientKey: "your-publishable-client-key", + tokenStore: "cookie", + }); + ``` + + + + + + + + + ### Install npm package + + + ```bash title="Terminal" + npm install @stackframe/js + ``` + + + ### Update API keys + + Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its values into the `stack/server.ts` or `stack/client.ts` file. + + + ### Initialize the app + + + + Server + Client + + + + ```typescript title="stack/server.ts" + import { StackServerApp } from "@stackframe/js"; + + const stackServerApp = new StackServerApp({ + // You should store these in environment variables based on your project setup + projectId: "your-project-id-from-dashboard", + publishableClientKey: "your-publishable-client-key-from-dashboard", + secretServerKey: "your-secret-server-key-from-dashboard", + tokenStore: "memory", + }); + ``` + + + + ```tsx title="stack/client.ts" + import { StackClientApp } from "@stackframe/js"; + + const stackClientApp = new StackClientApp({ + // You should store these in environment variables based on your project setup + projectId: "your-project-id", + publishableClientKey: "your-publishable-client-key", + tokenStore: "cookie", + }); + ``` + + + + + + +## Example usage + + + + Server + Client + + + + ```typescript + import { stackServerApp } from "@/stack/server"; + + const user = await stackServerApp.getUser("user_id"); + + await user.update({ + displayName: "New Display Name", + }); + + const team = await stackServerApp.createTeam({ + name: "New Team", + }); + + await team.addUser(user.id); + ``` + + + + ```typescript + import { stackClientApp } from "@/stack/client"; + + await stackClientApp.signInWithCredential({ + email: "test@example.com", + password: "password123", + }); + + const user = await stackClientApp.getUser(); + + await user.update({ + displayName: "New Display Name", + }); + + await user.signOut(); + ``` + + + +## Next steps + +Check out the [Users](./users.mdx) to learn how to retrieve and update user information, or [Example pages](./example-pages.mdx) to see how to build your sign-in/up pages. + + +{/* ELSE_IF_PLATFORM python */} + + +Welcome to the Python setup guide. If you're looking for guides for other frameworks, check out the [Next.js SDK Setup](/next/getting-started/setup), [React SDK Setup](/react/getting-started/setup), or the [JavaScript SDK Setup](/js/getting-started/setup). + + +Our recommended way to use Stack Auth with Python is with the [REST API](../rest-api/overview.mdx). It provides a fully documented way to interact with Stack Auth from any Python framework, including Flask, FastAPI, and Django. + +For the purpose of this guide, we will use the `requests` library to make HTTP requests to the Stack Auth API. If you haven't already, you can install it in your environment with `pip install requests`. + + + ### Create API keys + + First, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), and copy your project ID, publishable client key, and secret server key into a safe place (eg. environment variables). + + From there, you can access them in your Python code. You can then read them like this: + + ```python + import os + + stack_project_id = os.getenv("STACK_PROJECT_ID") + stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") + stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY") + ``` + + ### Make a request + + Next, create a helper function to make requests to the Stack Auth API: + + ```python + import requests + + def stack_auth_request(method, endpoint, **kwargs): + res = requests.request( + method, + f'https://api.stack-auth.com/{endpoint}', + headers={ + 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API + 'x-stack-project-id': stack_project_id, + 'x-stack-publishable-client-key': stack_publishable_client_key, + 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' + **kwargs.pop('headers', {}), + }, + **kwargs, + ) + if res.status_code >= 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json() + + print(stack_auth_request('GET', '/api/v1/projects/current')) + ``` + ### Retrieve the access tokens + + If you're building a backend server, most likely you'll want to use the currently signed in user's access token. Most normally, you would send this with all your requests to the backend in an HTTP header. + + In Stack Auth's JavaScript SDK, you can retrieve the access token [from the `stackClientApp` object](/sdk/types/user#currentusergetauthjson). Then, you can use said access token to make requests to Stack Auth: + + ```python + access_token = # access token retrieved from the JavaScript SDK + + print(stack_auth_request('GET', '/api/v1/users/me', headers={ + 'x-stack-access-token': access_token, + })) + ``` + + ### Done! + + +## Next steps + +Check out the [REST API documentation](../rest-api/overview.mdx) to learn more about the available endpoints and how to use them in your Python application. + +{/* END_PLATFORM */} + diff --git a/docs/content/docs/(guides)/getting-started/users.mdx b/docs/content/docs/(guides)/getting-started/users.mdx new file mode 100644 index 0000000000..a41364f8f2 --- /dev/null +++ b/docs/content/docs/(guides)/getting-started/users.mdx @@ -0,0 +1,253 @@ +--- +title: Users +icon: "users" +--- + +You will inevitably build custom components that access the user in one way or another. In this section, we will take a closer look at the functions and hooks that let you do this. + +## Client Component basics + +The `useUser()` hook returns the current user in a Client Component. By default, it will return `null` if the user is not signed in. + +```tsx title="my-client-component.tsx" +"use client"; +import { useUser } from "@stackframe/stack" + +export function MyClientComponent() { + const user = useUser(); + return
{user ? `Hello, ${user.displayName ?? "anon"}` : 'You are not logged in'}
; +} +``` + +The `useUser()` hook is simply a shorthand for `useStackApp().useUser()`. `useStackApp()` also contains other useful hooks and methods for clients, which will be described later. + +Sometimes, you want to retrieve the user only if they're signed in, and redirect to the sign-in page otherwise. In this case, simply pass `{ or: "redirect" }`, and the function will never return `null`. + +```tsx + const user = useUser({ or: "redirect" }); + return
{`Hello, ${user.displayName ?? "anon"}`}
; +``` + +## Server Component basics + +Since `useUser()` is a stateful hook, you can't use it on server components. Instead, you can import `stackServerApp` from `stack/client.ts` and call `getUser()`: + +```tsx title="my-server-component.tsx" +import { stackServerApp } from "@/stack/server"; + +export default async function MyServerComponent() { + const user = await stackServerApp.getUser(); // or: stackServerApp.getUser({ or: "redirect" }) + return
{user ? `Hello, ${user.displayName ?? "anon"}` : 'You are not logged in'}
; +} +``` + + +Since `useUser()` is a hook, it will re-render the component on user changes (eg. signout), while `getUser()` will only fetch the user once (on page load). You can also call `useStackApp().getUser()` on the client side to get the user in a non-component context. + + + +## Protecting a page + +There are three ways to protect a page: in Client Components with `useUser({ or: "redirect" })`, in Server Components with `await getUser({ or: "redirect" })`, or with middleware. + +On Client Components, the `useUser({ or: 'redirect' })` hook will redirect the user to the sign-in page if they are not logged in. Similarly, on Server Components, call `await getUser({ or: "redirect" })` to protect a page (or component). + +Middleware can be used whenever it is easy to tell whether a page should be protected given just the URL, for example, when you have a `/private` section only accessible to logged-in users. + + + + + Client Component + Server Component + Middleware + + + + ```tsx title="my-protected-client-component.tsx" + "use client"; + import { useUser } from "@stackframe/stack"; + + export default function MyProtectedClientComponent() { + useUser({ or: 'redirect' }); + return

You can only see this if you are logged in

+ } + ``` +
+ + + ```tsx title="my-protected-server-component.tsx" + import { stackServerApp } from "@/stack/server"; + + export default async function MyProtectedServerComponent() { + await stackServerApp.getUser({ or: 'redirect' }); + return

You can only see this if you are logged in

+ } + ``` +
+ + + ```tsx title="middleware.tsx" + export async function middleware(request: NextRequest) { + const user = await stackServerApp.getUser(); + if (!user) { + return NextResponse.redirect(new URL('/handler/sign-in', request.url)); + } + return NextResponse.next(); + } + + export const config = { + // You can add your own route protection logic here + // Make sure not to protect the root URL, as it would prevent users from accessing static Next.js files or Stack's /handler path + matcher: '/protected/:path*', + }; + ``` + +
+ + + If you have sensitive information hidden in the page HTML itself, be aware of Next.js differences when using Server vs. Client Components. + + - **Client Components**: Client components are always sent to the browser, regardless of page protection. This is standard Next.js behavior. For more information, please refer to the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment). + + - **Server Components**: If a component is protected, it is guaranteed that its bundled HTML will not be sent to the browser if the user is not logged in. However, this is not necessarily true for its children and the rest of the page, as Next.js may split components on the same page and send them to the client separately for performance. + + For example, if your page is ``, where `Parent` is protected and `Child` is not, Next.js may still send `` to the browser even if the user is not logged in. (Normal browsers will never display it, but attackers may be able to retrieve it.) Notably, this also applies to unprotected pages inside protected layouts. + + To remediate this, every component/page that contains sensitive information should protect itself, instead of relying on an outer layout. This is good practice anyways; it prevents you from accidentally exposing the data. + + - **Middleware**: Prior to Next.js v15.2.3, Next.js allowed attackers to see unprotected components if you only protect on a middleware level. Since v15.2.3, this is no longer possible, and you don't have to worry about leaking sensitive information when using middleware to protect a route. + + No matter which method you use, attackers will never be able to, say, impersonate a user. + + + + +## User data + +You can update attributes on a user object with the `user.update()` function. + +```tsx title="my-client-component.tsx" +'use client'; +import { useUser } from "@stackframe/stack"; + +export default function MyClientComponent() { + const user = useUser(); + return ; +} +``` + +You can also store custom user data in the `clientMetadata`, `serverMetadata`, or `clientReadonlyMetadata` fields. More information [here](../concepts/custom-user-data). + +## Signing out + +You can sign out the user by redirecting them to `/handler/sign-out` or simply by calling `user.signOut()`. They will be redirected to the URL [configured as `afterSignOut` in the `StackServerApp`](../sdk/objects/stack-app). + + + + user.signOut() + Redirect + + + + ```tsx title="sign-out-button.tsx" + "use client"; + import { useUser } from "@stackframe/stack"; + + export default function SignOutButton() { + const user = useUser(); + return user ? : "Not signed in"; + } + ``` + + + + ```tsx title="sign-out-link.tsx" + import { stackServerApp } from "@/stack/server"; + + export default async function SignOutLink() { + // stackServerApp.urls.signOut is equal to /handler/sign-out + return Sign Out; + } + ``` + + + +## Example: Custom profile page + +Stack automatically creates a user profile on sign-up. Let's build a page that displays this information. In `app/profile/page.tsx`: + + + + Client Component + Server Component + + + + ```tsx title="app/profile/page.tsx" + 'use client'; + import { useUser, useStackApp, UserButton } from "@stackframe/stack"; + + export default function PageClient() { + const user = useUser(); + const app = useStackApp(); + return ( +
+ {user ? ( +
+ +

Welcome, {user.displayName ?? "unnamed user"}

+

Your e-mail: {user.primaryEmail}

+ +
+ ) : ( +
+

You are not logged in

+ + +
+ )} +
+ ); + } + ``` +
+ + + ```tsx title="app/profile/page.tsx" + import { stackServerApp } from "@/stack/server"; + import { UserButton } from "@stackframe/stack"; + + export default async function Page() { + const user = await stackServerApp.getUser(); + return ( +
+ {user ? ( +
+ +

Welcome, {user.displayName ?? "unnamed user"}

+

Your e-mail: {user.primaryEmail}

+

Sign Out

+
+ ) : ( +
+

You are not logged in

+

Sign in

+

Sign up

+
+ )} +
+ ); + } + ``` +
+
+ +After saving your code, you can see the profile page on [http://localhost:3000/profile](http://localhost:3000/profile). + +For more examples on how to use the `User` object, check the [the SDK documentation](../sdk/types/user.mdx). + +## Next steps + +In the next guide, we will show you how to put [your application into production](./production.mdx). diff --git a/docs/content/docs/(guides)/meta.json b/docs/content/docs/(guides)/meta.json new file mode 100644 index 0000000000..1767574811 --- /dev/null +++ b/docs/content/docs/(guides)/meta.json @@ -0,0 +1,41 @@ +{ + "title": "Stack Auth Guides", + "description": "Stack Auth Documentation", + "root": true, + "pages": [ + "overview", + "faq", + "---Getting Started---", + "getting-started/setup", + "getting-started/components", + "getting-started/users", + "getting-started/production", + "---Concepts---", + "concepts/api-keys", + "concepts/backend-integration", + "concepts/custom-user-data", + "concepts/emails", + "concepts/jwt", + "concepts/oauth", + "concepts/auth-providers", + "concepts/orgs-and-teams", + "concepts/permissions", + "concepts/stack-app", + "concepts/team-selection", + "concepts/user-onboarding", + "concepts/webhooks", + "---Customization---", + "customization/custom-pages", + "customization/custom-styles", + "customization/dark-mode", + "customization/internationalization", + "customization/page-examples", + "---Other---", + "others/cli-authentication", + "others/self-host", + "others/supabase", + "others/convex", + "sdk", + "components" + ] +} diff --git a/docs/content/docs/(guides)/others/cli-authentication.mdx b/docs/content/docs/(guides)/others/cli-authentication.mdx new file mode 100644 index 0000000000..e956327137 --- /dev/null +++ b/docs/content/docs/(guides)/others/cli-authentication.mdx @@ -0,0 +1,63 @@ +--- +title: CLI Authentication +description: How to authenticate a command line application using Stack Auth +--- + +If you're building a command line application that runs in a terminal, you can use Stack Auth to let your users log in to their accounts. + +To do so, we provide a Python template that you can use as a starting point. [Download it here](https://github.com/stack-auth/stack-auth/tree/main/docs/public/stack_auth_cli_template.py) and copy it into your project, for example: + +```py +└─ my-python-app + ├─ main.py + └─ stack_auth_cli_template.py # <- the file you just downloaded +``` + +Then, you can import the `prompt_cli_login` function: + +```py +from stack_auth_cli_template import prompt_cli_login + +# prompt the user to log in +refresh_token = prompt_cli_login( + app_url="https://your-app-url.example.com", + project_id="your-project-id-here", + publishable_client_key="your-publishable-client-key-here", +) + +if refresh_token is None: + print("User cancelled the login process. Exiting") + exit(1) + +# you can also store the refresh token in a file, and only prompt the user to log in if the file doesn't exist + +# you can now use the REST API with the refresh token +def stack_auth_request(method, endpoint, **kwargs): + # ... see Stack Auth's Getting Started section to see how this function should look like + # https://docs.stack-auth.com/python/getting-started/setup + +def get_access_token(refresh_token): + access_token_response = stack_auth_request( + 'post', + '/api/v1/auth/sessions/current/refresh', + headers={ + 'x-stack-refresh-token': refresh_token, + } + ) + + return access_token_response['access_token'] + +def get_user_object(access_token): + return stack_auth_request( + 'get', + '/api/v1/users/me', + headers={ + 'x-stack-access-token': access_token, + } + ) + +user = get_user_object(get_access_token(refresh_token)) +print("The user is logged in as", user['display_name'] or user['primary_email']) +``` + + diff --git a/docs/content/docs/(guides)/others/convex.mdx b/docs/content/docs/(guides)/others/convex.mdx new file mode 100644 index 0000000000..27a9ffb400 --- /dev/null +++ b/docs/content/docs/(guides)/others/convex.mdx @@ -0,0 +1,101 @@ +--- +title: Convex +description: Integrate Stack Auth with Convex +--- + +This guide shows how to integrate Stack Auth with Convex. + +### 1) Create a Convex + Next.js app + +```bash title="Terminal" +npx create-next-app --example convex stack-convex +cd stack-convex +npx @stackframe/init-stack@latest +``` + +Add your Stack environment variables to both `.env.local` and convex env: +- `NEXT_PUBLIC_STACK_PROJECT_ID` +- `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY` +- `STACK_SECRET_SERVER_KEY` + +### 2) Update `convex/auth.config.ts` + +Use the exported helper to configure Convex auth providers for Stack. If `NEXT_PUBLIC_STACK_PROJECT_ID` is set, the `projectId` option is optional. + +```typescript title="convex/auth.config.ts" +import { getConvexProvidersConfig } from "@stackframe/stack"; + +export default { + providers: getConvexProvidersConfig({ + // Optional: projectId: PROJECT_ID, + }), +}; +``` + +### 3) Create your Stack client + +```typescript title="stack/client.ts" +import { StackClientApp } from "@stackframe/stack"; + +export const stackClientApp = new StackClientApp({ + tokenStore: "nextjs-cookie", +}); +``` + +### 4) Use with Convex clients + + + + Convex React client + Convex HTTP client + + + + + ### Set auth for Convex React client + + ```tsx title="convex-react.ts" + import { ConvexReactClient } from "convex/react"; + import { stackClientApp } from "../stack/client"; + + const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!, { expectAuth: true }); + convex.setAuth( + stackClientApp.getConvexClientAuth({ tokenStore: "nextjs-cookie" }) + ); + ``` + + + + + + ### Set auth for Convex HTTP client + + ```ts title="convex-http.ts" + import { ConvexHttpClient } from "convex/browser"; + import { stackClientApp } from "../stack/client"; + + const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" }); + const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + convex.setAuth(token); + ``` + + + + +### 5) Use in Convex functions (server) + +In Convex queries/mutations/actions, map Convex identity to a Stack partial user with `ctx`: + +```ts title="convex/myFunctions.ts" +import { v } from "convex/values"; +import { query } from "./_generated/server"; +import { stackClientApp } from "../stack/client"; + +export const whoAmI = query({ + args: {}, + handler: async (ctx) => { + const user = await stackClientApp.getPartialUser({ from: "convex", ctx }); + return user; // null when not authenticated + }, +}); +``` diff --git a/docs/content/docs/(guides)/others/self-host.mdx b/docs/content/docs/(guides)/others/self-host.mdx new file mode 100644 index 0000000000..76411e87d0 --- /dev/null +++ b/docs/content/docs/(guides)/others/self-host.mdx @@ -0,0 +1,159 @@ +--- +title: Self-host +--- + +Stack Auth is fully open-source and can be self-hosted on your own infrastructure. This guide will introduce each component of the project and how to set them up. + + +If you are unsure whether you should self-host, here are some things to consider: + +- **Complexity**: Stack Auth is a complex project with many interdependent services. Self-hosting requires managing these services and ensuring they work together seamlessly. +- **Updates**: Stack Auth is a rapidly evolving project with frequent feature and fix releases. Self-hosting requires you to manage updates and apply them timely. +- **Reliability**: Self-hosting requires you to ensure the reliability of your infrastructure. Downtimes and outages can be costly to handle. +- **Security**: Self-hosting requires ensuring the security of your infrastructure. A compromised service can affect your users. + +For most users, we recommend using [Stack Auth's cloud hosted solution](https://app.stack-auth.com). However, if you understand the above challenges and are comfortable managing them, follow the instructions below to self-host! + + +## Services + +On a high level, Stack Auth is composed of the following services: +- **API backend**: The core of Stack Auth, providing the REST API that the dashboard and your app connect to. This is what [api.stack-auth.com](https://api.stack-auth.com) provides. +- **Dashboard**: The interface for managing users, teams, auth methods, etc. This is available at [app.stack-auth.com](https://app.stack-auth.com). +- **Client SDK**: An SDK used to connect your app to the Stack Auth API backend, wrapping API calls and providing easy-to-use interfaces. More details [here](../getting-started/setup.mdx). +- **Postgres database**: Used to store all user data. We use [Prisma](https://prisma.io) as the ORM and manage the database schema migrations. +- **Svix**: Used to send webhooks. Svix is open-source and can be self-hosted, but also offers a cloud hosted solution. More on Svix [here](https://svix.com) +- **Email server**: We use [Inbucket](https://inbucket.org) as a local email server for development and a separate SMTP server for production. Any email service supporting SMTP will work. +- **S3 storage**: We use [S3Mock](https://github.com/adobe/S3Mock) as a local S3 storage for development and a separate S3 storage for production. Any S3 storage will work. + +## Run with Docker + +Stack Auth provides a [pre-configured Docker](https://hub.docker.com/r/stackauth/server) image that bundles the dashboard and API backend into a single container. To complete the setup, you'll need to provide your own PostgreSQL database, and optionally configure an email server and Svix instance for webhooks. + +1. Use a cloud hosted Postgres or start a example Postgres database. Don't use this setting in production: + ```sh + docker run -d --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=stackframe -p 5432:5432 postgres:latest + ``` + +2. Get the [example environment file](https://github.com/stack-auth/stack-auth/tree/main/docker/server/.env.example) and modify it to your needs (for security, you MUST edit at least the `STACK_SERVER_SECRET` value). You must also supply a `STACK_FREESTYLE_API_KEY` in order to send emails with Stack Auth (can be generated on [freestyle](https://freestyle.sh)). See the [full template here](https://github.com/stack-auth/stack-auth/blob/dev/docker/server/.env). + +3. Run the Docker container: + ```sh + docker run --env-file -p 8101:8101 -p 8102:8102 stackauth/server:latest + ``` + + + For M-series Mac users, you might need to add `--platform linux/x86_64` to the `docker run` command. + + + For Linux users, you might need to add `--add-host=host.docker.internal:host-gateway` to the `docker run` command in order to connect to the local Postgres database. + + +Now you can open the dashboard at [http://localhost:8101](http://localhost:8101) and the API backend on port 8102. + +Now, login with your admin account on the dashboard and follow the [normal setup process](../getting-started/setup.mdx). Add `NEXT_PUBLIC_STACK_API_URL=https://your-backend-url.com` to your app's environment variables so that it connects to your API backend instead of the default Stack Auth API backend (https://api.stack-auth.com). + +## Local development + +### Setup + +Clone the repository and check out the directory: + +```sh +git clone git@github.com:stack-auth/stack.git +cd stack +``` + +Pre-populated .env files for the setup below are available and used by default in `.env.development` in each of the packages. (Note: If you're creating a production build (eg. with `pnpm run build`), you must supply the environment variables manually.) + +In a new terminal: + +```sh +pnpm install + +# Run build to build everything once +pnpm run build:dev + +# reset & start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema +pnpm run start-deps +# pnpm run restart-deps +# pnpm run stop-deps + +# Start the dev server +pnpm run dev +# For systems with limited resources, you can run a minimal development setup with just the backend and dashboard +# pnpm run dev:basic + +# In a different terminal, run tests in watch mode +pnpm run test +``` + +You can now open the dev launchpad at [http://localhost:8100](http://localhost:8100). From there, you can navigate to the dashboard at [http://localhost:8101](http://localhost:8101), API on port 8102, demo on port 8103, docs on port 8104, Inbucket (e-mails) on port 8105, and Prisma Studio on port 8106. See the dev launchpad for a list of all running services. + +Your IDE may show an error on all `@stackframe/XYZ` imports. To fix this, simply restart the TypeScript language server; for example, in VSCode you can open the command palette (Ctrl+Shift+P) and run `Developer: Reload Window` or `TypeScript: Restart TS server`. + +You can also open Prisma Studio to see the database interface and edit data directly: + +```sh +pnpm run prisma studio +``` + +## Run individual services + +### Database, Svix, email + +Deploy these services with your preferred platform. Copy the URLs/API keys—you'll need them in the next step. + +### API backend + +Clone the repository and check out the root directory: + +```sh +git clone git@github.com:stack-auth/stack.git +cd stack +``` + +Set all the necessary environment variables (you can check out `apps/backend/.env`). Note that `NEXT_PUBLIC_STACK_API_URL` should be the URL of your deployed domain (e.g., https://your-backend-url.com). + +Build and start the server: + +```sh +pnpm install +pnpm build:backend +pnpm start:backend +``` + +### Dashboard + +Clone the repository (if you are running it on a separate server, or skip this step if you are using the same server as the API backend) and check out the dashboard directory: + +```sh +git clone git@github.com:stack-auth/stack.git +cd stack +``` + +Set all the necessary environment variables (you can check out `apps/dashboard/.env`). Note that `NEXT_PUBLIC_STACK_API_URL` should be the URL of your deployed backend (e.g., https://your-backend-url.com). + +Build and start the server: + +```sh +pnpm install +pnpm build:dashboard +pnpm start:dashboard +``` + +### Initialize the database + +You need to initialize the database with the following command with the backend environment variables set: + +```sh +pnpm db:init +``` + +Now you can go to the dashboard (e.g., https://your-dashboard-url.com) and sign up for an account. + +To manage your dashboard configs with this account, manually go into the database, find the user you just created, and add `{ managedProjectIds: ["internal"] }` to the `serverMetadata` jsonb column. + +Go back to the dashboard, refresh the page, and you should see the "Stack Dashboard" project. We recommend disabling new user sign-ups to your internal project to avoid unauthorized account and project creations. + +Now, create a new project for your app and follow the [normal setup process](../getting-started/setup.mdx). Add `NEXT_PUBLIC_STACK_API_URL=https://your-backend-url.com` to your app's environment variables so that it connects to your API backend instead of the default Stack Auth API backend (https://api.stack-auth.com). diff --git a/docs/content/docs/(guides)/others/supabase.mdx b/docs/content/docs/(guides)/others/supabase.mdx new file mode 100644 index 0000000000..87472e5863 --- /dev/null +++ b/docs/content/docs/(guides)/others/supabase.mdx @@ -0,0 +1,164 @@ +--- +title: Supabase +description: Integrate Stack Auth with Supabase RLS +--- + +This guide shows how to integrate Stack Auth with Supabase row level security (RLS). + + + This guide only focuses on the RLS/JWT integration and does not sync user data between Supabase and Stack. You should use [webhooks](/concepts/webhooks) to achieve data sync. + + +## Setup + +Let's create a sample table and some RLS policies to demonstrate how to integrate Stack Auth with Supabase RLS. You can apply the same logic to your own tables and policies. + +### Setup Supabase +First, let's create a Supabase project, then go to the [SQL Editor](https://supabase.com/dashboard/project/_/sql/new) and create a new table with some sample data and RLS policies. + +```sql title="Supabase SQL Editor" +-- Create the 'data' table +CREATE TABLE data ( + id bigint PRIMARY KEY, + text text NOT NULL, + user_id UUID +); + +-- Insert sample data +INSERT INTO data (id, text, user_id) VALUES + (1, 'Everyone can see this', NULL), + (2, 'Only authenticated users can see this', NULL), + (3, 'Only user with specific id can see this', NULL); + +-- Enable Row Level Security +ALTER TABLE data ENABLE ROW LEVEL SECURITY; + +-- Allow everyone to read the first row +CREATE POLICY "Public read" ON "public"."data" TO public +USING (id = 1); + +-- Allow authenticated users to read the second row +CREATE POLICY "Authenticated access" ON "public"."data" TO authenticated +USING (id = 2); + +-- Allow only the owner of the row to read it +CREATE POLICY "User access" ON "public"."data" TO authenticated +USING (id = 3 AND auth.uid() = user_id); +``` + +### Setup a new Next.js project + +Now let's create a new Next.js project and install Stack Auth and Supabase client. (more details on [Next.js setup](https://nextjs.org/docs/getting-started/installation), [Stack Auth setup](../getting-started/setup.mdx), and [Supabase setup](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs)) + +```bash title="Terminal" +npx create-next-app@latest -e with-supabase stack-supabase +cd stack-supabase +npx @stackframe/init-stack@latest +``` + +Now copy the environment variables from the Supabase dashboard to the `.env.local` file: +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- `SUPABASE_JWT_SECRET` + +Copy environment variables from the Stack dashboard to the `.env.local` file. +- `NEXT_PUBLIC_STACK_PROJECT_ID` +- `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY` +- `STACK_SECRET_SERVER_KEY` + +### Set up Supbase client + +Now let's create a server action that mints a supabase JWT with the Stack Auth user ID if the user is authenticated. + +```tsx title="/utils/actions.ts" +'use server'; + +import { stackServerApp } from "@/stack/server"; +import * as jose from "jose"; + +export const getSupabaseJwt = async () => { + const user = await stackServerApp.getUser(); + + if (!user) { + return null; + } + + const token = await new jose.SignJWT({ + sub: user.id, + role: "authenticated", + }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime('1h') + .sign(new TextEncoder().encode(process.env.SUPABASE_JWT_SECRET)); + + return token; +}; +``` + +And now create a helper function to create a Supabase client with the JWT signed by the server action + +```tsx title="/utils/supabase-client.ts" +import { createBrowserClient } from "@supabase/ssr"; +import { getSupabaseJwt } from "./actions"; + +export const createSupabaseClient = () => { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { accessToken: async () => await getSupabaseJwt() || "" } + ); +} +``` + +### Fetch data from Supabase + +Let's create an example page that fetches data from Supabase and displays it. + +```tsx title="/app/page.tsx" +'use client'; + +import { createSupabaseClient } from "@/utils/supabase-client"; +import { useStackApp, useUser } from "@stackframe/stack"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +export default function Page() { + const app = useStackApp(); + const user = useUser(); + const supabase = createSupabaseClient(); + const [data, setData] = useState(null); + + useEffect(() => { + supabase.from("data").select().then(({ data }) => setData(data ?? [])); + }, []); + + const listContent = data === null ? +

Loading...

: + data.length === 0 ? +

No notes found

: + data.map((note) =>
  • {note.text}
  • ); + + return ( +
    + { + user ? + <> +

    You are signed in

    +

    User ID: {user.id}

    + Sign Out + : + Sign In + } +

    Supabase data

    +
      {listContent}
    +
    + ) +} +``` + +Now you should be able to compare the data you can view with an anonymous user, an authenticated user. You can also add your user Id to the row 3 of the Supabase table, and you should be able to see the row if and only if you are signed in with that user. + +
    + +You can find the full example [here on GitHub](https://github.com/stack-auth/stack-auth/tree/main/examples/supabase). diff --git a/docs/content/docs/(guides)/overview.mdx b/docs/content/docs/(guides)/overview.mdx new file mode 100644 index 0000000000..d06ea0f95d --- /dev/null +++ b/docs/content/docs/(guides)/overview.mdx @@ -0,0 +1,112 @@ +--- +title: Overview +--- + +Welcome to Stack Auth, the developer-friendly open-source authentication platform that gets you started in minutes! + +You can get started in five minutes with our [setup guide](./getting-started/setup.mdx), or jump straight into the documentation. + + + + Get started with Stack in 5 minutes + + {/* IF_PLATFORM: react-like */} + + Use our pre-built React components, or create your own + + {/* END_PLATFORM */} + {/* IF_PLATFORM: js-like */} + + Learn how to use Stack Auth's SDK + + {/* END_PLATFORM */} + + Explore Stack's REST APIs + + + +Still have questions? Check out our [FAQ](./faq) or [join our Discord](https://discord.stack-auth.com). + +## Why Choose Stack Auth? + +Authentication is inherently difficult. Few things are more sensitive than user data and more complex than cryptography. Not surprisingly, many online businesses struggle to get it right. + +The optimal authentication solution should be secure, yet approachable. If developers need to worry about JWTs, OAuth flows, or password hashing, then the authentication solution has failed its purpose. If an authentication solution uses closed-source, unauditable code for the most critical parts of your application, then we have failed. + +In truth, the authentication services industry has collectively failed. It's dominated by proprietary giants with predatory "bait-and-switch" pricing, providing no transparency into their codebase and delivering a subpar developer experience — because they know enterprises will pay more if setting up auth systems is painful. + +That's why we built Stack Auth. Integrating secure authentication into your app should take **5 minutes**, not 5 days. + +At the core of Stack Auth are deep integrations into frontend and backend frameworks. We offer the best developer experience with Next.js. Instead of providing mediocre support for numerous frameworks, we focused on making a few integrations excellent before adding new ones. We also offer a cross-compatible REST API as a fallback. + +Here's an example. To get the current user, simply call: + +```tsx +export function MyComponent() { + const user = useUser({ or: "redirect" }); + return
    Hi, {user.displayName}
    ; +} +``` + +That's it! Stack Auth will either return a User object or redirect the user to the login page. + +You can also add a button to change the user's name: + +```tsx + +``` + +The user data will update in both the frontend and backend automatically. The updated user data will be reflected in all other components on your page as well. + +You also get pages and components for the authentication flow out-of-the-box. This is the sign-in page you get without writing a single line of code: + +
    + SignIn +
    + +Notice, there's no branding on our components. We believe we should grow by building the best product, not by forcing our brand onto your users. This means **we depend on your help to spread the word about Stack**. If you like what you're reading, please take a moment to tell one or two of your friends about us. + +If you prefer a fully customized UI, you can use our low-level functions like `signInWithOAuth` or `signInWithCredential` to build your own sign-in page: + +```tsx +export default function CustomOAuthSignIn() { + const app = useStackApp(); + return ( +
    + +
    + ); +} +``` + +To manage everything efficiently, there is a powerful admin dashboard: + +![Stack dashboard](/imgs/dashboard.png) + +Best of all, Stack Auth is **100% open-source**. This means the client, server, dashboard, and even this documentation you're reading right now. Check out our [GitHub](https://github.com/stack-auth/stack-auth) to open an issue or pull request. + +This is just a glimpse of what Stack Auth can do. We also handle many other tasks like backend integration, data storage, emails, teams, permissions, and more, which you will learn about later in the documentation. + +If this sounds interesting, [get started](./getting-started/setup) with our interactive setup wizard, or join [our Discord community](https://discord.stack-auth.com) to ask questions and get help from our team. + +We're excited to have you on board! 🚀 diff --git a/docs/content/docs/(guides)/rest-api/overview.mdx b/docs/content/docs/(guides)/rest-api/overview.mdx new file mode 100644 index 0000000000..1a85e84bb7 --- /dev/null +++ b/docs/content/docs/(guides)/rest-api/overview.mdx @@ -0,0 +1,98 @@ +--- +title: API Reference +description: Complete REST API documentation for Stack Auth +full: true +--- + +Stack offers a REST API for backends & frontends of any programming language or framework. This API is used to authenticate users, manage user data, and more. + +## Authentication + +The following authentication headers are common to every endpoint: + +```http +curl https://api.stack-auth.com/api/v1/ \ + -H "X-Stack-Access-Type: " \ + -H "X-Stack-Project-Id: " \ + -H "X-Stack-Publishable-Client-Key: pck_" \ + -H "X-Stack-Secret-Server-Key: ssk_" \ + -H "X-Stack-Access-Token: " +``` + +| Header | Type | Description | +| ------ | ---- | ----------- | +| `X-Stack-Access-Type` | "client" \| "server" | Required. "client" (without quotes) for the frontend API, or "server" for the backend API. | +| `X-Stack-Project-Id` | UUID | Required. The project ID as found on the Stack dashboard. | +| `X-Stack-Publishable-Client-Key` | string | Required for client access. The API key as found on the Stack dashboard. | +| `X-Stack-Secret-Server-Key` | string | Required for server access. The API key as found on the Stack dashboard. | +| `X-Stack-Access-Token` | string | Optional. The access token for the current user. If not given, the request is considered to be logged out. | + + +{/* IF_PLATFORM python */} +To see how to use these headers in various programming languages, see the [Getting Started guide](./../getting-started/setup.mdx). +{/* ELSE_IF_PLATFORM js-like */} +To see how to use these headers in various programming languages, see the [examples](./../concepts/backend-integration.mdx). +{/* END_IF_PLATFORM */} + + +## Getting Started + + + + **Choose the right API**: Select the API category that matches your use case from the cards above + + + **Set up authentication**: Configure the appropriate authentication method (sessions, API keys, or webhook verification) + + + **Make requests**: Use the documented endpoints with proper authentication headers + + + **Handle responses**: Process the API responses according to the documentation and error handling guidelines + + + +## FAQ + + + + Any language that has the ability to send HTTP requests can use the Stack REST API. This includes JavaScript, Python, Ruby, Java, Go, C#, Dart, and many more. + + + + **Client access type** is mostly used for client-side applications, like a browser or mobile app. The client APIs can only read and update the currently authenticated user's data, and it is usually fine to post the publishable client key in the client-side code. + + **Server access type**, on the other hand, is for your backend server that you control. It has full access over all user data, and the secret server key should never be exposed to client-side code. + + For more information, see the concept documentation on [StackApp](../concepts/stack-app#client-vs-server). + + + + If you'd like to build your own version of the Stack dashboard (or update project configuration programmatically), you can use the `admin` access type. These endpoints are very dangerous and you should only use them if you know what you're doing. + + For more information, see the concept documentation on [StackApp](../concepts/stack-app#client-vs-server). + + + + Stack Auth API returns standard HTTP status codes. Common error responses include: + + - `400 Bad Request` - Invalid request parameters + - `401 Unauthorized` - Invalid or missing authentication + - `403 Forbidden` - Insufficient permissions + - `404 Not Found` - Resource not found + - `429 Too Many Requests` - Rate limit exceeded + - `500 Internal Server Error` - Server error + + Error responses include a JSON body with additional details about the error. + + + + Yes, Stack Auth implements rate limiting to ensure fair usage and system stability. Rate limits vary by endpoint and access type. When you exceed the rate limit, you'll receive a `429 Too Many Requests` response with headers indicating when you can retry. + + + +## Need Help? + +- Check out our [Getting Started Guide](/docs/next/getting-started/setup) for initial setup +- Visit our [Concepts](/docs/next/concepts) section to understand Stack Auth fundamentals +- Join our [Discord community](https://discord.stack-auth.com/) for support and discussions diff --git a/docs/content/docs/components/account-settings.mdx b/docs/content/docs/components/account-settings.mdx new file mode 100644 index 0000000000..4e81412098 --- /dev/null +++ b/docs/content/docs/components/account-settings.mdx @@ -0,0 +1,71 @@ +--- +title: "" +full: true +--- + +## Demo + + + +## Props + + + +## Example + +```tsx +import { AccountSettings } from '@stackframe/stack'; + +export default function MyAccountPage() { + return ( + , + content: , + subpath: '/custom', + }]} + /> + ); +} +``` diff --git a/docs/content/docs/components/credential-sign-in.mdx b/docs/content/docs/components/credential-sign-in.mdx new file mode 100644 index 0000000000..c1e1ffa9a3 --- /dev/null +++ b/docs/content/docs/components/credential-sign-in.mdx @@ -0,0 +1,32 @@ +--- +title: "" +--- + +A component that renders a sign-in form with email and password fields. + +
    + CredentialSignIn +
    + +Note that if credential sign-in is disabled in the dashboard, this component will still render. However, attempting to use it will result in an error being thrown. + +For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). + +## Props + +This component does not accept any props. + +## Example + +```tsx +import { CredentialSignIn } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Sign In

    + +
    + ); +} +``` diff --git a/docs/content/docs/components/credential-sign-up.mdx b/docs/content/docs/components/credential-sign-up.mdx new file mode 100644 index 0000000000..d804d0a933 --- /dev/null +++ b/docs/content/docs/components/credential-sign-up.mdx @@ -0,0 +1,42 @@ +--- +title: "" +--- + +A component that renders a sign-up form with email and password fields. + +
    + CredentialSignUp +
    + +Note that if credential sign-up is disabled in the dashboard, this component will still render. However, attempting to use it will result in an error being thrown. + +For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). + +## Props + + + +## Example + +```tsx +import { CredentialSignUp } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Sign Up

    + +
    + ); +} +``` diff --git a/docs/content/docs/components/forgot-password.mdx b/docs/content/docs/components/forgot-password.mdx new file mode 100644 index 0000000000..1d202bc615 --- /dev/null +++ b/docs/content/docs/components/forgot-password.mdx @@ -0,0 +1,29 @@ +--- +title: "" +--- + +Renders a forgot password component with options for full-page display. + +## Props + + + +## Example + +```tsx +import { ForgotPassword } from '@stackframe/stack'; + +export const MyForgotPassword = () => { + return ; +} +``` diff --git a/docs/content/docs/components/index.mdx b/docs/content/docs/components/index.mdx new file mode 100644 index 0000000000..9df168d153 --- /dev/null +++ b/docs/content/docs/components/index.mdx @@ -0,0 +1,150 @@ +--- +title: Components Overview +--- + +Stack Auth provides a set of components for Next.js applications. + +To get started with Stack Auth in your Next.js application, follow the [setup guide](../getting-started/setup.mdx). To see the hooks and objects in the Next.js SDK, see the [SDK reference](../sdk). + +
    + + + + + ## Sign In and Sign Up + + + +
    +
    + SignIn +
    + <SignIn /> +
    +
    + +
    +
    + SignUp +
    + <SignUp /> +
    +
    + +
    +
    + CredentialSignIn +
    + <CredentialSignIn /> +
    +
    + +
    +
    + CredentialSignUp +
    + <CredentialSignUp /> +
    +
    + +
    +
    + OAuthButton +
    + <OAuthButton /> +
    +
    + +
    +
    + OAuthButtonGroup +
    + <OAuthButtonGroup /> +
    +
    +
    + + + ## User + + + +
    +
    + UserButton +
    + <UserButton /> +
    +
    + +
    +
    + AccountSettings +
    + <AccountSettings /> +
    +
    +
    + + + ## Teams & Organizations + + + +
    +
    + SelectedTeamSwitcher +
    + <SelectedTeamSwitcher /> +
    +
    +
    + + ## Utilities + + + +
    + <StackHandler /> +
    +
    + +
    + <StackProvider /> +
    +
    + +
    + <StackTheme /> +
    +
    +
    +
    diff --git a/docs/content/docs/components/magic-link-sign-in.mdx b/docs/content/docs/components/magic-link-sign-in.mdx new file mode 100644 index 0000000000..f8fa902fda --- /dev/null +++ b/docs/content/docs/components/magic-link-sign-in.mdx @@ -0,0 +1,24 @@ +--- +title: "" +--- + +A component that provides a magic link sign-in form with email input and OTP verification. + +## Props + +This component does not accept any props. + +## Example + +```tsx +import { MagicLinkSignIn } from '@stackframe/stack'; + +export function SignInPage() { + return ( +
    +

    Sign In

    + +
    + ); +} +``` diff --git a/docs/content/docs/components/meta.json b/docs/content/docs/components/meta.json new file mode 100644 index 0000000000..60b7c8684f --- /dev/null +++ b/docs/content/docs/components/meta.json @@ -0,0 +1,27 @@ +{ + "title": "Components", + "description": "Pre-built React components", + "root": true, + "pages": [ + "index", + "---Authentication---", + "sign-in", + "sign-up", + "credential-sign-in", + "credential-sign-up", + "magic-link-sign-in", + "forgot-password", + "password-reset", + "---OAuth---", + "oauth-button", + "oauth-button-group", + "---User Interface---", + "user-button", + "account-settings", + "selected-team-switcher", + "---Layout & Providers---", + "stack-provider", + "stack-handler", + "stack-theme" + ] +} diff --git a/docs/content/docs/components/oauth-button-group.mdx b/docs/content/docs/components/oauth-button-group.mdx new file mode 100644 index 0000000000..d37bd95dc2 --- /dev/null +++ b/docs/content/docs/components/oauth-button-group.mdx @@ -0,0 +1,41 @@ +--- +title: "" +--- + +Renders all the OAuth buttons enabled on the project dashboard. + +
    + OAuthButtonGroup +
    + + +If there are no OAuth providers enabled in the dashboard, this component will be empty. + + +## Props + + + +## Example + +```tsx +import { OAuthButtonGroup } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Sign In

    + +
    + ); +} + +``` diff --git a/docs/content/docs/components/oauth-button.mdx b/docs/content/docs/components/oauth-button.mdx new file mode 100644 index 0000000000..2683c7b496 --- /dev/null +++ b/docs/content/docs/components/oauth-button.mdx @@ -0,0 +1,49 @@ +--- +title: "" +--- + +Renders a customized OAuth button for various providers to initiate sign-in or sign-up processes. + +
    + OAuthButton +
    + +Note that if the specific OAuth provider is disabled in the dashboard, this component will still render. However, attempting to use it will result in an error being thrown. + +For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). + +## Props + + + +## Example + +```tsx +import { OAuthButton } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Sign In

    + + +
    + ); +} + +``` + +The `OAuthButton` component automatically styles itself based on the provided OAuth provider and handles the sign-in or sign-up process when clicked. diff --git a/docs/content/docs/components/password-reset.mdx b/docs/content/docs/components/password-reset.mdx new file mode 100644 index 0000000000..cd84a07ade --- /dev/null +++ b/docs/content/docs/components/password-reset.mdx @@ -0,0 +1,39 @@ +--- +title: "" +--- + +Renders a password reset component based on the provided search parameters and optional full page display. + +## Props + +", + description: "An object containing search parameters, including the password reset code." + }, + { + name: "fullPage", + type: "boolean", + description: "Determines whether to display the component in full page mode.", + optional: true, + default: "false" + } + ]} +/> + +## Example + +```tsx title="app/reset-password.tsx" +import { PasswordReset } from '@stackframe/stack'; + +export function ResetPasswordPage(props: { searchParams: Record }) { + return ( + + ); +} +``` diff --git a/docs/content/docs/components/selected-team-switcher.mdx b/docs/content/docs/components/selected-team-switcher.mdx new file mode 100644 index 0000000000..b9bdaf684d --- /dev/null +++ b/docs/content/docs/components/selected-team-switcher.mdx @@ -0,0 +1,56 @@ +--- +title: "" +--- + + + +For a comprehensive guide on using this component, refer to our [Team Selection documentation](../concepts/team-selection.mdx). + +## Props + + + + +## Example + +```tsx +import { SelectedTeamSwitcher } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Team Switcher

    + `/team/${team.id}`} + selectedTeam={currentTeam} + noUpdateSelectedTeam={false} + /> +
    + ); +} +``` diff --git a/docs/content/docs/components/sign-in.mdx b/docs/content/docs/components/sign-in.mdx new file mode 100644 index 0000000000..8a901a5e12 --- /dev/null +++ b/docs/content/docs/components/sign-in.mdx @@ -0,0 +1,65 @@ +--- +title: "" +full: true +--- + +Renders a sign-in component with customizable options. + + + +For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). + +## Props + + + +## Example + +```tsx +import { SignIn } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Sign In

    + When signing in, you agree to our Terms} + /> +
    + ); +} +``` diff --git a/docs/content/docs/components/sign-up.mdx b/docs/content/docs/components/sign-up.mdx new file mode 100644 index 0000000000..32248dafaf --- /dev/null +++ b/docs/content/docs/components/sign-up.mdx @@ -0,0 +1,71 @@ +--- +title: "" +--- + +A component that renders a sign-up page with various customization options. + +
    + SignUp +
    + +For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). + +## Props + + + +## Example + +```tsx +import { SignUp } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    Sign Up

    + By signing up, you agree to our Terms} + /> +
    + ); +} +``` diff --git a/docs/content/docs/components/stack-handler.mdx b/docs/content/docs/components/stack-handler.mdx new file mode 100644 index 0000000000..042e649e1a --- /dev/null +++ b/docs/content/docs/components/stack-handler.mdx @@ -0,0 +1,56 @@ +--- +title: "" +--- + +Renders the appropriate authentication or account-related component based on the current route. + +For detailed usage instructions, please refer to the manual section of the [setup guide](../getting-started/setup.mdx). + +## Props + +> }", + description: "Props to pass to the rendered components." + } + ]} +/> + +## Example + +```tsx title="app/handler/[...stack].tsx" +import { StackHandler } from '@stackframe/stack'; +import { stackServerApp } from "@/stack/server"; + +export default function Handler(props: { params: any, searchParams: any }) { + return ( + + ); +} +``` diff --git a/docs/content/docs/components/stack-provider.mdx b/docs/content/docs/components/stack-provider.mdx new file mode 100644 index 0000000000..cdffb66629 --- /dev/null +++ b/docs/content/docs/components/stack-provider.mdx @@ -0,0 +1,58 @@ +--- +title: "" +--- + +A React component that provides Stack context to its children. + +For detailed usage instructions, please refer to the manual section of the [setup guide](../getting-started/setup.mdx). + +## Props + +", + description: "A mapping of English translations to translated equivalents. These will take priority over the translations from the language specified in the lang property. Note that the keys are case-sensitive. You can find a full list of supported strings on GitHub.", + optional: true + } + ]} +/> + +## Example + +```tsx title="layout.tsx" +import { StackProvider } from '@stackframe/stack'; +import { stackServerApp } from '@/stack/server'; + +function App() { + return ( + + {/* Your app content */} + + ); +} +``` diff --git a/docs/content/docs/components/stack-theme.mdx b/docs/content/docs/components/stack-theme.mdx new file mode 100644 index 0000000000..48755c1b22 --- /dev/null +++ b/docs/content/docs/components/stack-theme.mdx @@ -0,0 +1,47 @@ +--- +title: "" +--- + +A component that applies a theme to its children. + +For more information, please refer to the [color and styles guide](../customization/custom-styles.mdx). + +## Props + + + +## Example + +```tsx +const theme = { + light: { + primary: 'red', + }, + dark: { + primary: '#00FF00', + }, + radius: '8px', +} + +// ... + + + {/* children */} + + +``` diff --git a/docs/content/docs/components/user-button.mdx b/docs/content/docs/components/user-button.mdx new file mode 100644 index 0000000000..4ef6672ab1 --- /dev/null +++ b/docs/content/docs/components/user-button.mdx @@ -0,0 +1,79 @@ +--- +title: "" +--- + +The `` renders a user button component with optional user information, color mode toggle, and extra menu items. + +## Interactive Demo + +Try out the UserButton component with different props and see the changes in real-time: + + + +## Props + + void | Promise", + description: "Function to be called when the color mode toggle button is clicked. If specified, a color mode toggle button will be shown.", + optional: true, + default: "undefined" + }, + { + name: "extraItems", + type: "Array", + description: "Additional menu items to display. Each item should have the following properties:", + optional: true, + nested: [ + { + name: "text", + type: "string", + description: "The text to display for the item." + }, + { + name: "icon", + type: "React.ReactNode", + description: "The icon to display for the item." + }, + { + name: "onClick", + type: "() => void | Promise", + description: "Function to be called when the item is clicked." + } + ] + } + ]} +/> + +## Example + +```tsx +'use client'; +import { UserButton } from '@stackframe/stack'; + +export default function Page() { + return ( +
    +

    User Button

    + { console.log("color mode toggle clicked") }} + extraItems={[{ + text: 'Custom Action', + icon: , + onClick: () => console.log('Custom action clicked') + }]} + /> +
    + ); +} +``` diff --git a/docs/content/docs/sdk/hooks/use-stack-app.mdx b/docs/content/docs/sdk/hooks/use-stack-app.mdx new file mode 100644 index 0000000000..3e55c03fb6 --- /dev/null +++ b/docs/content/docs/sdk/hooks/use-stack-app.mdx @@ -0,0 +1,16 @@ +--- +title: useStackApp +--- + +The `useStackApp` hook returns a `StackClientApp` object from the one that you provided in the [`StackProvider` component](../../components/stack-provider.mdx). If you want to learn more about the `StackClientApp` object, check out the [StackApp](../objects/stack-app.mdx) documentation. + +Example: + +```jsx +import { useStackApp } from "@stackframe/stack"; + +function MyComponent() { + const stackApp = useStackApp(); + return
    Sign In URL: {stackApp.urls.signIn}
    ; +} +``` diff --git a/docs/content/docs/sdk/hooks/use-user.mdx b/docs/content/docs/sdk/hooks/use-user.mdx new file mode 100644 index 0000000000..a094b20d42 --- /dev/null +++ b/docs/content/docs/sdk/hooks/use-user.mdx @@ -0,0 +1,7 @@ +--- +title: useUser +--- + +This standalone React hook is an alias for `useStackApp().useUser()`. It only exists for convenience; it does not have any additional functionality. + +For more information, please refer to the [documentation for `stackClientApp.useUser()`](../objects/stack-app.mdx#stackclientappuseuseroptions). diff --git a/docs/content/docs/sdk/index.mdx b/docs/content/docs/sdk/index.mdx new file mode 100644 index 0000000000..a85c720d88 --- /dev/null +++ b/docs/content/docs/sdk/index.mdx @@ -0,0 +1,57 @@ +--- +title: SDK Overview +--- + + +This is the SDK reference for Stack Auth's Next.js SDK. + +For a list of components, see the [Components](../components/overview) page. For instructions on how to get started and how to use the SDK, see the [Setup & Installation](../getting-started/setup.mdx) page. If you are using a framework or programming language other than Next.js, you can use [our REST API](/api/overview). + +export const sdkSections = [ + { + title: "General", + items: [ + { name: "StackClientApp", href: "objects/stack-app#stackclientapp", icon: "object" }, + { name: "StackServerApp", href: "objects/stack-app#stackserverapp", icon: "object" }, + { name: "Project", href: "types/project#project", icon: "type" }, + ] + }, + { + title: "Users & user data", + items: [ + { name: "CurrentUser", href: "types/user#currentuser", icon: "type" }, + { name: "ServerUser", href: "types/user#serveruser", icon: "type" }, + { name: "CurrentServerUser", href: "types/user#currentserveruser", icon: "type" }, + { name: "ContactChannel", href: "types/contact-channel#contactchannel", icon: "type" }, + { name: "ServerContactChannel", href: "types/contact-channel#servercontactchannel", icon: "type" }, + ] + }, + { + title: "Teams", + items: [ + { name: "Team", href: "types/team#team", icon: "type" }, + { name: "ServerTeam", href: "types/team#serverteam", icon: "type" }, + { name: "TeamPermission", href: "types/team-permission#teampermission", icon: "type" }, + { name: "ServerTeamPermission", href: "types/team-permission#serverteampermission", icon: "type" }, + { name: "TeamUser", href: "types/team-user#teamuser", icon: "type" }, + { name: "ServerTeamUser", href: "types/team-user#serverteamuser", icon: "type" }, + { name: "TeamProfile", href: "types/team-profile#teamprofile", icon: "type" }, + { name: "ServerTeamProfile", href: "types/team-profile#serverteamprofile", icon: "type" }, + ] + }, + { + title: "Email", + items: [ + { name: "SendEmailOptions", href: "types/email#sendemailoptions", icon: "type" }, + ] + }, + { + title: "Hooks", + items: [ + { name: "useStackApp", href: "hooks/use-stack-app", icon: "hook" }, + { name: "useUser", href: "hooks/use-user", icon: "hook" }, + ] + } +]; + + diff --git a/docs/content/docs/sdk/meta.json b/docs/content/docs/sdk/meta.json new file mode 100644 index 0000000000..0e64521f08 --- /dev/null +++ b/docs/content/docs/sdk/meta.json @@ -0,0 +1,25 @@ +{ + "title": "SDK Reference", + "description": "SDK reference documentation", + "root": true, + "pages": [ + "index", + "---Objects---", + "objects/stack-app", + "objects/stack-app-test", + "---Types---", + "types/user", + "types/team", + "types/team-user", + "types/team-permission", + "types/team-profile", + "types/contact-channel", + "types/email", + "types/api-key", + "types/project", + "types/connected-account", + "---Hooks---", + "hooks/use-stack-app", + "hooks/use-user" + ] +} diff --git a/docs/content/docs/sdk/objects/stack-app.mdx b/docs/content/docs/sdk/objects/stack-app.mdx new file mode 100644 index 0000000000..7b2e9c10db --- /dev/null +++ b/docs/content/docs/sdk/objects/stack-app.mdx @@ -0,0 +1,987 @@ +--- +title: StackApp +full: true +--- + +This is a detailed reference for the `StackApp` object. If you're looking for a more high-level overview, please read the [respective page in the Concepts section](../../concepts/stack-app.mdx). + +## Overview + +- [StackClientApp](#stackclientapp) - Client-level permissions for frontend code +- [StackServerApp](#stackserverapp) - Server-level permissions with full access + +--- + +# StackClientApp + +A [`StackApp`](../../concepts/stack-app.mdx) with client-level permissions. It contains most of the useful methods and hooks for your client-side code. + +{/* IF_PLATFORM: react-like */} +Most commonly you get an instance of `StackClientApp` by calling [`useStackApp()`](../hooks/use-stack-app.mdx) in a Client Component. +{/* END_PLATFORM */} + +## Table of Contents + ; //$stack-link-to:#stackclientappgetuseroptions + // NEXT_LINE_PLATFORM react-like + ⤷ useUser([options]): User; //$stack-link-to:#stackclientappuseuseroptions + getProject(): Promise; //$stack-link-to:#stackclientappgetproject + // NEXT_LINE_PLATFORM react-like + ⤷ useProject(): Project; //$stack-link-to:#stackclientappuseproject + + signInWithOAuth(provider): void; //$stack-link-to:#stackclientappsigninwithoauthprovider + signInWithCredential([options]): Promise<...>; //$stack-link-to:#stackclientappsigninwithcredentialoptions + signUpWithCredential([options]): Promise<...>; //$stack-link-to:#stackclientappsignupwithcredentialoptions + sendForgotPasswordEmail(email): Promise<...>; //$stack-link-to:#stackclientappsendforgotpasswordemailemail + sendMagicLinkEmail(email): Promise<...>; //$stack-link-to:#stackclientappsendmagiclinkemailemail + };`} /> + +## Constructor + + + + + +Creates a new `StackClientApp` instance. + +Because each app creates a new connection to Stack Auth's backend, you should re-use existing instances wherever possible. + +{/* IF_PLATFORM: react-like */} + +This object is not usually constructed directly. More commonly, you would construct a [`StackServerApp`](#stackserverapp) instead, pass it into a [``](../../components/stack-provider.mdx), and then use `useStackApp()` hook to obtain a `StackClientApp`. + +The [setup wizard](../../getting-started/setup.mdx) does these steps for you, so you don't need to worry about it unless you are manually setting up Stack Auth. + +If you're building a client-only app and don't have a [`SECRET_SERVER_KEY`](../../rest-api/overview#should-i-use-client-or-server-access-type), you can construct a `StackClientApp` directly. + +{/* END_PLATFORM */} + +**Parameters:** + +
    + + An object containing multiple properties. + + + + + +
    + +
    + + + +```typescript +declare new(options: { + tokenStore: "nextjs-cookie" | "cookie" | { accessToken: string, refreshToken: string } | Request; + baseUrl?: string; + projectId?: string; + publishableClientKey?: string; + urls: { + ... + }; + noAutomaticPrefetch?: boolean; +}): StackClientApp; +``` + + + + + + + Creating new app + Using useStackApp + + +```typescript +const stackClientApp = new StackClientApp({ + tokenStore: "nextjs-cookie", + baseUrl: "https://api.stack-auth.com", + projectId: "123", + publishableClientKey: "123", + urls: { + home: "/", + }, +}); +``` + + +{/* IF_PLATFORM: react-like */} +```typescript +"use client"; + +function MyReactComponent() { + const stackClientApp = useStackApp(); +} +``` +{/* END_PLATFORM */} + + + + +
    +
    + + + + + +Gets the current user. + +**Parameters:** +- `options?` (object) - Optional configuration + - `or?` - What to do if user not found: `"return-null"` | `"redirect"` | `"throw"` + +**Returns:** `Promise` - The current user, or `null` if not signed in + + + + + + +```typescript +declare function getUser( + options?: { + or?: "return-null" | "redirect" | "throw" + } +): Promise; +``` + + + +```typescript +// Basic usage +const userOrNull = await stackClientApp.getUser(); +console.log(userOrNull); // null if not signed in + +// With redirect on no user +const user = await stackClientApp.getUser({ or: "redirect" }); +console.log(user); // always defined; redirects to sign-in page if not signed in +``` + + + + + +{/* IF_PLATFORM: react-like */} + + + + +React hook version of `getUser()`. Functionally equivalent to [`getUser()`](#stackclientappgetuseroptions), but as a React hook. + +Equivalent to the [`useUser()`](../hooks/use-user.mdx) standalone hook (which is an alias for `useStackApp().useUser()`). + +**Parameters:** +- `options?` (object) - Same as `getUser()` + +**Returns:** `CurrentUser | null` + + + + + + +```typescript +declare function useUser( + options?: { + or?: "return-null" | "redirect" | "throw" + } +): CurrentUser | null; +``` + + + + + + Basic Usage + With Redirect + Page Protection + + +```jsx +"use client"; + +function MyReactComponent() { + const user = useUser(); + return user ?
    Hello, {user.name}
    + :
    Not signed in
    ; +} +``` +
    + +```tsx +"use client"; + +function MyReactComponent() { + const user = useUser(); + console.log(user); // null if not signed in + + const user = useUser({ or: "redirect" }); // redirects to sign-in page if necessary + console.log(user); // always defined + + const user = useUser({ or: "throw" }); // throws an error if not signed in + console.log(user); // always defined +} +``` + + +```tsx +"use client"; + +function MyProtectedComponent() { + // Note: This component is protected on the client-side. + // It does not protect against malicious users, since + // they can just comment out the `useUser` call in their + // browser's developer console. + // + // For server-side protection, see the Stack Auth documentation. + + useUser({ or: "redirect" }); + return
    You can only see this if you are authenticated
    ; +} +``` +
    +
    +
    +
    +
    +
    +{/* END_PLATFORM */} + + + + + +Gets the current project. + +**Parameters:** +- No parameters + +**Returns:** `Promise` + + + + + + +```typescript +declare function getProject(): Promise; +``` + + + +```typescript +const project = await stackClientApp.getProject(); +``` + + + + + +{/* IF_PLATFORM: react-like */} + + + +React hook version of `getProject()`. + +**Parameters:** +- No parameters + +**Returns:** `Project` + + + + + +```typescript +declare function useProject(): Project; +``` + + +getting the current project in a react component +```typescript +function MyReactComponent() { + const project = useProject(); +} +``` + + + + +{/* END_PLATFORM */} + + + + + +Initiates the OAuth sign-in process with the specified provider. + +**Parameters:** +- `provider` (string) - The OAuth provider type + +**Returns:** `Promise` + + + + + + +```typescript +declare function signInWithOAuth(provider: string): Promise; +``` + + + +```typescript +await stackClientApp.signInWithOAuth("google"); +``` + + + + + + + + + +Sign in using email and password credentials. + +**Parameters:** +- `options` (object) + - `email` (string) - User's email + - `password` (string) - User's password + - `noRedirect?` (boolean) - Whether to skip redirect after sign-in + +**Returns:** `Promise>` + + + + + + +```typescript +declare function signInWithCredential(options: { + email: string; + password: string; + noRedirect?: boolean; +}): Promise>; +``` + + + +```typescript +const result = await stackClientApp.signInWithCredential({ + email: "test@example.com", + password: "password", +}); + +if (result.status === "error") { + console.error("Sign in failed", result.error.message); +} +``` + + + + + + + + + +Sign up using email and password credentials. + +**Parameters:** +- `options` (object) + - `email` (string) - User's email + - `password` (string) - User's password + - `noRedirect?` (boolean) - Whether to skip redirect after sign-up + +**Returns:** `Promise>` + + + + + + +```typescript +declare function signUpWithCredential(options: { + email: string; + password: string; + noRedirect?: boolean; +}): Promise>; +``` + + + +```typescript +const result = await stackClientApp.signUpWithCredential({ + email: "test@example.com", + password: "password", +}); + +if (result.status === "error") { + console.error("Sign up failed", result.error.message); +} +``` + + + + + + + + + +Send a forgot password email to an email address. + +**Parameters:** +- `email` (string) - The email to send the forgot password email to + +**Returns:** `Promise>` + + + + + + +```typescript +declare function sendForgotPasswordEmail(email: string): Promise>; +``` + + + +```typescript +const result = await stackClientApp.sendForgotPasswordEmail("test@example.com"); + +if (result.status === "success") { + console.log("Forgot password email sent"); +} else { + console.error("Failed to send forgot password email", result.error.message); +} +``` + + + + + + + + + +Send a magic link/OTP sign-in email to an email address. + +**Parameters:** +- `email` (string) - The email to send the magic link to + +**Returns:** `Promise>` + + + + + + +```typescript +declare function sendMagicLinkEmail(email: string): Promise>; +``` + + + +```typescript +const result = await stackClientApp.sendMagicLinkEmail("test@example.com"); +``` + + + + + +--- + +# StackServerApp + +Like `StackClientApp`, but with [server permissions](../../concepts/stack-app.mdx#client-vs-server). Has full read and write access to all users. + + +Since this functionality should only be available in environments you trust (ie. your own server), it requires a [`SECRET_SERVER_KEY`](../../rest-api/overview.mdx). +In some cases, you may want to use a [`StackServerApp`](#stackserverapp) on the client; an example for this is an internal dashboard that only your own employees have access to. +We generally recommend against doing this unless you are aware of and protected against the (potentially severe) security implications of +exposing [`SECRET_SERVER_KEY`](../../rest-api/overview.mdx) on the client. + + +## Table of Contents + ; //$stack-link-to:#stackserverappgetuseridoptions + // NEXT_LINE_PLATFORM react-like + ⤷ useUser([id][, options]): ServerUser; //$stack-link-to:#stackserverappuseuseridoptions + listUsers([options]): Promise; //$stack-link-to:#stackserverapplistusersoptions + // NEXT_LINE_PLATFORM react-like + ⤷ useUsers([options]): ServerUser[]; //$stack-link-to:#stackserverappuseusersoptions + createUser([options]): Promise; //$stack-link-to:#stackserverappcreateuseroptions + sendEmail(options): Promise>; //$stack-link-to:#stackserverappsendemailoptions + + getTeam(id): Promise; //$stack-link-to:#stackserverappgetteamid + // NEXT_LINE_PLATFORM react-like + ⤷ useTeam(id): ServerTeam; //$stack-link-to:#stackserverappuseteamid + listTeams(): Promise; //$stack-link-to:#stackserverapplistteams + // NEXT_LINE_PLATFORM react-like + ⤷ useTeams(): ServerTeam[]; //$stack-link-to:#stackserverappuseteams + createTeam([options]): Promise; //$stack-link-to:#stackserverappcreateteamoptions + }`} /> + +## Constructor + + + + + Creates a new `StackClientApp` instance. + +**Parameters:** + + + An object containing multiple properties. + + + + The secret server key of the app, as found on Stack Auth's dashboard. Defaults to the value of the `SECRET_SERVER_KEY` environment variable. + + + + + + + + + +```typescript +declare new(options: { + tokenStore: "nextjs-cookie" | "cookie" | { accessToken: string, refreshToken: string } | Request; + baseUrl?: string; + projectId?: string; + publishableClientKey?: string; + urls: { + ... + }; + noAutomaticPrefetch?: boolean; +}): StackServerApp; +``` + + + + + + Create a StackServerApp with a custom sign-in page + + + ```typescript + const stackServerApp = new StackServerApp({ + tokenStore: "nextjs-cookie", + urls: { + signIn: '/my-custom-sign-in-page', + }, + }); + ``` + + + + + + + + + + + + +Enhanced version of `StackClientApp.getUser()` with server permissions. + +**Overloads:** +1. `getUser(id: string): Promise` - Get user by ID +2. `getUser(options?: { or?: "return-null" | "redirect" | "throw" }): Promise` - Get current user + + + + + + +```typescript +// This function has two overloads: +declare function getUser(id: string): Promise; +declare function getUser( + options?: { + or?: "return-null" | "redirect" | "throw" + } +): Promise; +``` + + + + + + Get Current User + Get User by ID + + +```typescript +const user = await stackServerApp.getUser(); +console.log(user); // CurrentServerUser +``` + + +```typescript +const user = await stackServerApp.getUser("12345678-1234-1234-1234-123456789abc"); +console.log(user); // ServerUser +``` + + + + + + + +{/* IF_PLATFORM react-like */} + +Functionally equivalent to [`getUser()`](#stackserverappgetuserid-options), but as a React hook. + + +This should be used on the server-side only. + + +{/* END_PLATFORM */} + + + + + +Lists all users on the project. + +**Parameters:** + +
    + + An object containing multiple properties. + + + The cursor to start the result set from. + + + The maximum number of items to return. If not provided, it will return all users. + + + The field to sort the results by. Currently, only `signedUpAt` is supported. + + + Whether to sort the results in descending order. + + + A query to filter the results by. This is a free-text search on the user's display name and emails. + + + +
    + +**Returns:** `Promise` + +
    + + + + +```typescript +declare function listUsers(options?: { + cursor?: string; + limit?: number; + orderBy?: "signedUpAt"; + desc?: boolean; + query?: string; +}): Promise; +``` + + + +```typescript +const users = await stackServerApp.listUsers({ limit: 20 }); +console.log(users); + +if (users.nextCursor) { + const nextPageUsers = await stackServerApp.listUsers({ + cursor: users.nextCursor, + limit: 20 + }); + console.log(nextPageUsers); +} +``` + + +
    +
    + +{/* IF_PLATFORM react-like */} + + +Functionally equivalent to [`listUsers()`](#stackserverapplistusersoptions), but as a React hook. + + This should be used on the server-side only. + + +{/* END_PLATFORM */} + + + + + + +Creates a new user from the server. + +**Parameters:** +- `options?` (object) + - `primaryEmail?` (string) - User's primary email + - `primaryEmailVerified?` (boolean) - Whether email is verified + - `primaryEmailAuthEnabled?` (boolean) - Whether email auth is enabled + - `password?` (string) - User's password + - `otpAuthEnabled?` (boolean) - Enable OTP/magic link auth + - `displayName?` (string) - User's display name + +**Returns:** `Promise` + + + + + + + +```typescript +declare function createUser(options?: { + primaryEmail?: string; + primaryEmailVerified?: boolean; + primaryEmailAuthEnabled?: boolean; + password?: string; + otpAuthEnabled?: boolean; + displayName?: string; +}): Promise; +``` + + + + + + Password Auth + Magic Link Auth + + +```typescript +const user = await stackServerApp.createUser({ + primaryEmail: "test@example.com", + primaryEmailAuthEnabled: true, + password: "password123", +}); +``` + + +```typescript +const user = await stackServerApp.createUser({ + primaryEmail: "test@example.com", + primaryEmailVerified: true, + primaryEmailAuthEnabled: true, + otpAuthEnabled: true, +}); +``` + + + + + + + + + + + + + Send custom emails to users. You can send either custom HTML emails or use predefined templates with variables. + + **Parameters:** + - `options` ([SendEmailOptions](../types/email#sendemailoptions)) - Email configuration and content + + **Returns:** `Promise>` + + The method returns a `Result` object that can contain specific error types: + + - `RequiresCustomEmailServer` - No custom email server configured + - `SchemaError` - Invalid email data provided + - `UserIdDoesNotExist` - One or more user IDs don't exist + + + + + + +```typescript +declare function sendEmail(options: SendEmailOptions): Promise>; +``` + + + + + + Send HTML Email + Send Template Email + + +```typescript +const result = await stackServerApp.sendEmail({ + userIds: ['user-1', 'user-2'], + subject: 'Welcome to our platform!', + html: '

    Welcome!

    Thanks for joining us.

    ', +}); + +if (result.status === 'error') { + console.error('Failed to send email:', result.error); +} +``` +
    + +```typescript +const result = await stackServerApp.sendEmail({ + userIds: ['user-1'], + templateId: 'welcome-template', + variables: { + userName: 'John Doe', + activationUrl: 'https://app.com/activate/token123', + }, +}); + +if (result.status === 'error') { + console.error('Failed to send email:', result.error); +} +``` + +
    + +
    + +
    +
    +
    + +## Team Management + + + + + +Get a team by its ID. + +**Parameters:** +- `id` (string) - Team ID + +**Returns:** `Promise` + + + + + + +```typescript +declare function getTeam(id: string): Promise; +``` + + + +```typescript +const team = await stackServerApp.getTeam("team_id_123"); +``` + + + + + +{/* IF_PLATFORM react-like */} + + +Functionally equivalent to [`getTeam(id)`](#stackserverappgetteamid), but as a React hook. + + This should be used on the server-side only. + + +{/* END_PLATFORM */} + + + + + Lists all teams on the current project. + +**Returns:** `Promise` + + + + + ```typescript + declare function listTeams(): Promise; + ``` + + + + ```typescript + const teams = await stackServerApp.listTeams(); + console.log(teams); + ``` + + + + + +{/* IF_PLATFORM react-like */} + + +Functionally equivalent to [`listTeams()`](#stackserverapplistteams), but as a React hook. + + This should be used on the server-side only. + + +{/* END_PLATFORM */} + + + + + +Creates a team without adding a user to it. + +**Parameters:** +- `data` (object) + - `displayName` (string) - Team display name + - `profileImageUrl?` (string | null) - Team profile image URL + +**Returns:** `Promise` + + + + + + +```typescript +declare function createTeam(data: { + displayName: string; + profileImageUrl?: string | null; +}): Promise; +``` + + + +```typescript +const team = await stackServerApp.createTeam({ + displayName: "New Team", + profileImageUrl: "https://example.com/profile.jpg", +}); +``` + + + + diff --git a/docs/content/docs/sdk/overview-new.mdx b/docs/content/docs/sdk/overview-new.mdx new file mode 100644 index 0000000000..8609f00b48 --- /dev/null +++ b/docs/content/docs/sdk/overview-new.mdx @@ -0,0 +1,51 @@ +--- +title: SDK Overview +--- + + +This is the SDK reference for Stack Auth's Next.js SDK. + +For a list of components, see the [Components](../components) page. For instructions on how to get started and how to use the SDK, see the [Setup & Installation](../getting-started/setup.mdx) page. If you are using a framework or programming language other than Next.js, you can use [our REST API](../rest-api). + +export const sdkSections = [ + { + title: "General", + items: [ + { name: "StackClientApp", href: "./objects/stack-app#stackclientapp", icon: "object" }, + { name: "StackServerApp", href: "./sdk/objects/stack-app#stackserverapp", icon: "object" }, + { name: "Project", href: "./sdk/types/project#project", icon: "type" }, + ] + }, + { + title: "Users & user data", + items: [ + { name: "CurrentUser", href: "./sdk/types/user#currentuser", icon: "type" }, + { name: "ServerUser", href: "./sdk/types/user#serveruser", icon: "type" }, + { name: "CurrentServerUser", href: "./sdk/types/user#currentserveruser", icon: "type" }, + { name: "ContactChannel", href: "./sdk/types/contact-channel#contactchannel", icon: "type" }, + { name: "ServerContactChannel", href: "./sdk/types/contact-channel#servercontactchannel", icon: "type" }, + ] + }, + { + title: "Teams", + items: [ + { name: "Team", href: "./sdk/types/team#team", icon: "type" }, + { name: "ServerTeam", href: "./sdk/types/team#serverteam", icon: "type" }, + { name: "TeamPermission", href: "./sdk/types/team-permission#teampermission", icon: "type" }, + { name: "ServerTeamPermission", href: "./sdk/types/team-permission#serverteampermission", icon: "type" }, + { name: "TeamUser", href: "./sdk/types/team-user#teamuser", icon: "type" }, + { name: "ServerTeamUser", href: "./sdk/types/team-user#serverteamuser", icon: "type" }, + { name: "TeamProfile", href: "./sdk/types/team-profile#teamprofile", icon: "type" }, + { name: "ServerTeamProfile", href: "./sdk/types/team-profile#serverteamprofile", icon: "type" }, + ] + }, + { + title: "Hooks", + items: [ + { name: "useStackApp", href: "./sdk/hooks/use-stack-app", icon: "hook" }, + { name: "useUser", href: "./sdk/hooks/use-user", icon: "hook" }, + ] + } +]; + + diff --git a/docs/content/docs/sdk/types/api-key.mdx b/docs/content/docs/sdk/types/api-key.mdx new file mode 100644 index 0000000000..6d97cc2266 --- /dev/null +++ b/docs/content/docs/sdk/types/api-key.mdx @@ -0,0 +1,437 @@ +--- +title: ApiKey +full: true +--- + +`ApiKey` represents an authentication token that allows programmatic access to your application's backend. API keys can be associated with individual users or teams. + +On this page: +- [`ApiKey`](#apikey) +- Types: + - [`UserApiKey`](#userapikey) + - [`TeamApiKey`](#teamapikey) + +--- + +# `ApiKey` + +API keys provide a way for users to authenticate with your backend services without using their primary credentials. They can be created for individual users or for teams, allowing programmatic access to your application. + +API keys can be obtained through: +- [`user.createApiKey()`](../types/user.mdx#currentusercreateapikeyoptions) +- [`user.listApiKeys()`](../types/user.mdx#currentuserlistapikeys) +- [`user.useApiKeys()`](../types/user.mdx#currentuseruseapikeys) (React hook) +- [`team.createApiKey()`](../types/team.mdx#teamcreateapikeyoptions) +- [`team.listApiKeys()`](../types/team.mdx#teamlistapikeys) +- [`team.useApiKeys()`](../types/team.mdx#teamuseapikeys) (React hook) + +### Table of Contents + + = { + id: string; //$stack-link-to:#apikeyid + description: string; //$stack-link-to:#apikeydescription + expiresAt?: Date; //$stack-link-to:#apikeyexpiresat + manuallyRevokedAt: Date | null; //$stack-link-to:#apikeymanuallyrevokedat + createdAt: Date; //$stack-link-to:#apikeycreatedat + value: IsFirstView extends true ? string : { lastFour: string }; //$stack-link-to:#apikeyvalue + + // User or Team properties based on Type + ...(Type extends "user" ? { + type: "user"; + userId: string; //$stack-link-to:#apikeyuserid + } : { + type: "team"; + teamId: string; //$stack-link-to:#apikeyteamid + }) + + // Methods + isValid(): boolean; //$stack-link-to:#apikeyisvalid + whyInvalid(): "manually-revoked" | "expired" | null; //$stack-link-to:#apikeywhyinvalid + revoke(): Promise; //$stack-link-to:#apikeyrevoke + update(options): Promise; //$stack-link-to:#apikeyupdateoptions +};`} /> + +--- + + + + + The unique identifier for this API key. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + A human-readable description of the API key's purpose. + + + + ```typescript + declare const description: string; + ``` + + + + + + + + The date and time when this API key will expire. If not set, the key does not expire. + + + + ```typescript + declare const expiresAt?: Date; + ``` + + + + + + + + The date and time when this API key was manually revoked. If null, the key has not been revoked. + + + + ```typescript + declare const manuallyRevokedAt: Date | null; + ``` + + + + + + + + The date and time when this API key was created. + + + + ```typescript + declare const createdAt: Date; + ``` + + + + + + + + The value of the API key. When the key is first created, this is the full API key string. After that, only the last four characters are available for security reasons. + + + + ```typescript + // On first creation + declare const value: string; + + // On subsequent retrievals + declare const value: { lastFour: string }; + ``` + + + + + + + + For user API keys, the ID of the user that owns this API key. + + + + ```typescript + declare const userId: string; + ``` + + + + + + + + For team API keys, the ID of the team that owns this API key. + + + + ```typescript + declare const teamId: string; + ``` + + + + + + + + Checks if the API key is still valid (not expired and not revoked). + + ### Parameters + + None. + + ### Returns + + `boolean`: True if the key is valid, false otherwise. + + + + + ```typescript + declare function isValid(): boolean; + ``` + + + ```typescript Checking if an API key is valid + if (apiKey.isValid()) { + console.log("API key is still valid"); + } else { + console.log("API key is invalid"); + } + ``` + + + + + + + + + Returns the reason why the API key is invalid, or null if it is valid. + + ### Parameters + + None. + + ### Returns + + `"manually-revoked" | "expired" | null`: The reason the key is invalid, or null if it's valid. + + + + + ```typescript + declare function whyInvalid(): "manually-revoked" | "expired" | null; + ``` + + + ```typescript Checking why an API key is invalid + const reason = apiKey.whyInvalid(); + if (reason) { + console.log(`API key is invalid because it was ${reason}`); + } else { + console.log("API key is valid"); + } + ``` + + + + + + + + + Revokes the API key, preventing it from being used for authentication. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function revoke(): Promise; + ``` + + + ```typescript Revoking an API key + await apiKey.revoke(); + console.log("API key has been revoked"); + ``` + + + + + + + + + Updates the API key properties. + + ### Parameters + + + An object containing properties for updating. + + + A new description for the API key. + + + A new expiration date, or null to remove the expiration. + + + Set to true to revoke the API key. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function update(options: { + description?: string; + expiresAt?: Date | null; + revoked?: boolean; + }): Promise; + ``` + + + ```typescript Updating an API key + await apiKey.update({ + description: "Updated description", + expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days + }); + ``` + + + + + +--- + +# Types + + + + + A type alias for an API key owned by a user. + + + + ```typescript + type UserApiKey = ApiKey<"user", false>; + ``` + + + + + + + + A type alias for a newly created user API key, which includes the full key value instead of just the last four characters. + + + + ```typescript + type UserApiKeyFirstView = ApiKey<"user", true>; + ``` + + + + + + + + A type alias for an API key owned by a team. + + + + ```typescript + type TeamApiKey = ApiKey<"team", false>; + ``` + + + + + + + + A type alias for a newly created team API key, which includes the full key value instead of just the last four characters. + + + + ```typescript + type TeamApiKeyFirstView = ApiKey<"team", true>; + ``` + + + + +--- + +# Creation Options + +When creating an API key using [`user.createApiKey()`](../types/user.mdx#currentusercreatekeyoptions) or [`team.createApiKey()`](../types/team.mdx#teamcreatekeyoptions), you need to provide an options object. + + + + The options object for creating an API key. + + ### Properties + + + A human-readable description of the API key's purpose. + + + The date when the API key will expire. Use null for keys that don't expire. + + + Whether the API key is public. Defaults to false. + + - **Secret API Keys** (default) are monitored by Stack Auth's secret scanner, which can revoke them if detected in public code repositories. + - **Public API Keys** are designed for client-side code where exposure is not a concern. + + + + + + ```typescript + type ApiKeyCreationOptions = { + description: string; + expiresAt: Date | null; + isPublic?: boolean; + }; + ``` + + + + ```typescript Creating a user API key + // Get the current user + const user = await stackApp.getUser(); + + // Create a secret API key (default) + const secretKey = await user.createApiKey({ + description: "Backend integration", + expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days + isPublic: false, + }); + + // Create a public API key + const publicKey = await user.createApiKey({ + description: "Client-side access", + expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days + isPublic: true, + }); + ``` + + + + diff --git a/docs/content/docs/sdk/types/connected-account.mdx b/docs/content/docs/sdk/types/connected-account.mdx new file mode 100644 index 0000000000..aa5acc6e4b --- /dev/null +++ b/docs/content/docs/sdk/types/connected-account.mdx @@ -0,0 +1,5 @@ +--- +title: ConnectedAccount +--- + +## Placeholder diff --git a/docs/content/docs/sdk/types/contact-channel.mdx b/docs/content/docs/sdk/types/contact-channel.mdx new file mode 100644 index 0000000000..55c6c7fb7d --- /dev/null +++ b/docs/content/docs/sdk/types/contact-channel.mdx @@ -0,0 +1,308 @@ +--- +title: ContactChannel +full: true +--- + +`ContactChannel` represents a user's contact information, such as an email address or phone number. Some auth methods, like OTP/magic link or password, use contact channels for authentication. + +On this page: + +- [`ContactChannel`](#contactchannel) +- [`ServerContactChannel`](#servercontactchannel) + +# `ContactChannel` + +Basic information about a contact channel, as seen by a user themselves. + +Usually obtained by calling [`user.listContactChannels()`](../types/user.mdx#currentuserlistcontactchannels) +or [`user.useContactChannels()`](../types/user.mdx#currentuserusecontactchannels) {/* THIS_LINE_PLATFORM react-like */} +. + +### Table of Contents + +; //$stack-link-to:#contactchannelsendverificationemail + update(options): Promise; //$stack-link-to:#contactchannelupdateoptions + delete(): Promise; //$stack-link-to:#contactchanneldelete +};`} /> + + + + + The id of the contact channel as a `string`. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + The value of the contact channel. If type is `"email"`, this is an email address. + + + + ```typescript + declare const value: string; + ``` + + + + + + + + The type of the contact channel. Currently always `"email"`. + + + + ```typescript + declare const type: 'email'; + ``` + + + + + + + + Indicates whether the contact channel is the user's primary contact channel. If an email is set to primary, it will be the value on the `user.primaryEmail` field. + + + + ```typescript + declare const isPrimary: boolean; + ``` + + + + + + + + Indicates whether the contact channel is verified. + + + + ```typescript + declare const isVerified: boolean; + ``` + + + + + + + + Indicates whether the contact channel is used for authentication. If set to `true`, the user can use this contact channel with OTP or password to sign in. + + + + ```typescript + declare const usedForAuth: boolean; + ``` + + + + + + + + Sends a verification email to this contact channel. Once the user clicks the verification link in the email, the contact channel will be marked as verified. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function sendVerificationEmail(): Promise; + ``` + + + ```typescript Sending verification email + await contactChannel.sendVerificationEmail(); + ``` + + + + + + + + + Updates the contact channel. After updating the value, the contact channel will be marked as unverified. + + ### Parameters + + + An object containing properties for updating. + + + The new value of the contact channel. + + + The new type of the contact channel. Currently always `"email"`. + + + Indicates whether the contact channel is used for authentication. + + + Indicates whether the contact channel is the user's primary contact channel. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function update(options: { + value?: string; + type?: 'email'; + usedForAuth?: boolean; + isPrimary?: boolean; + }): Promise; + ``` + + + ```typescript Updating contact channel + await contactChannel.update({ + value: "new-email@example.com", + usedForAuth: true, + }); + ``` + + + + + + + + + Deletes the contact channel. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function delete(): Promise; + ``` + + + ```typescript Deleting contact channel + await contactChannel.delete(); + ``` + + + + + +# `ServerContactChannel` + +Like `ContactChannel`, but includes additional methods and properties that require the `SECRET_SERVER_KEY`. + +Usually obtained by calling [`serverUser.listContactChannels()`](../types/user.mdx#serveruserlistcontactchannels) +or [`serverUser.useContactChannels()`](../types/user.mdx#serveruserusecontactchannels) {/* THIS_LINE_PLATFORM react-like */} +. + +### Table of Contents + +; //$stack-link-to:#servercontactchannelupdateoptions + };`} /> + +--- + + + + + Updates the contact channel. + + This method is similar to the one on `ContactChannel`, but also allows setting the `isVerified` property. + + ### Parameters + + + An object containing properties for updating. + + + The new value of the contact channel. + + + The new type of the contact channel. Currently always `"email"`. + + + Indicates whether the contact channel is used for authentication. + + + Indicates whether the contact channel is verified. + + + Indicates whether the contact channel is the user's primary contact channel. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function update(options: { + value?: string; + type?: 'email'; + usedForAuth?: boolean; + isVerified?: boolean; + isPrimary?: boolean; + }): Promise; + ``` + + + ```typescript Updating server contact channel + await serverContactChannel.update({ + value: "new-email@example.com", + usedForAuth: true, + isVerified: true, + }); + ``` + + + + diff --git a/docs/content/docs/sdk/types/email.mdx b/docs/content/docs/sdk/types/email.mdx new file mode 100644 index 0000000000..9bca39435e --- /dev/null +++ b/docs/content/docs/sdk/types/email.mdx @@ -0,0 +1,201 @@ +--- +title: Email +full: true +--- + +This is a detailed reference for email-related types in Stack Auth. If you're looking for a more high-level overview, please refer to our [guide on the email system](../../concepts/emails.mdx). + +On this page: +- [SendEmailOptions](#sendemailoptions) + +--- + +# `SendEmailOptions` + +Options for sending emails via the `sendEmail` method on `StackServerApp`. + +### Table of Contents + +; //$stack-link-to:#sendemailoptionsvariables +};`} /> + + + + + An array of user IDs that will receive the email. All users must exist in your Stack Auth project. + + + + ```typescript + userIds: string[] + ``` + + + ```typescript + { + userIds: ['user-1', 'user-2', 'user-3'], + // ... other options + } + ``` + + + + + + + + + Optional theme ID to apply to the email. Use `null` for no theme, `false` to use the default theme, or a string ID for a specific theme. + + + + ```typescript + themeId?: string | null | false + ``` + + + ```typescript + { + themeId: 'corporate-theme-id', + // or + themeId: null, // no theme + // or + themeId: false, // default theme + // ... other options + } + ``` + + + + + + + + + Optional email subject line. If using a template, this overrides the template's default subject. + + + + ```typescript + subject?: string + ``` + + + ```typescript + { + subject: 'Welcome to our platform!', + // ... other options + } + ``` + + + + + + + + + Optional notification category name for user preferences. Users can opt in or out of specific categories through their account settings. + + + + ```typescript + notificationCategoryName?: string + ``` + + + ```typescript + { + notificationCategoryName: 'product_updates', + // ... other options + } + ``` + + + + + + + + + Custom HTML content for the email. Use this option when you want to send a custom HTML email instead of using a template. Cannot be used together with `templateId` or `variables`. + + + + ```typescript + html?: string + ``` + + + ```typescript + { + userIds: ['user-1'], + html: '

    Welcome!

    Thanks for joining us.

    ', + subject: 'Welcome to our platform' + } + ``` +
    +
    +
    +
    + + + + + ID of the email template to use. Use this option when you want to send a template-based email with variables. Cannot be used together with `html`. + + + + ```typescript + templateId?: string + ``` + + + ```typescript + { + userIds: ['user-1'], + templateId: 'welcome-template', + variables: { + userName: 'John Doe', + activationUrl: 'https://app.com/activate/token123' + } + } + ``` + + + + + + + + + Optional variables to substitute in the template. Only used when `templateId` is provided. + + + + ```typescript + variables?: Record + ``` + + + ```typescript + { + templateId: 'welcome-template', + variables: { + userName: 'John Doe', + activationUrl: 'https://app.com/activate/token123', + supportEmail: 'support@yourapp.com' + } + } + ``` + + + + diff --git a/docs/content/docs/sdk/types/project.mdx b/docs/content/docs/sdk/types/project.mdx new file mode 100644 index 0000000000..cbb2079f87 --- /dev/null +++ b/docs/content/docs/sdk/types/project.mdx @@ -0,0 +1,90 @@ +--- +title: Project +full: true +--- + +The `Project` object contains the information and configuration of a project, such as the name, description, and enabled authentication methods. + +Each [Stack app](../../concepts/stack-app.mdx) corresponds to a project. You can obtain its `Project` object by calling [`stackApp.getProject()`](../objects/stack-app.mdx#stackappgetproject) +or [`stackApp.useProject()`](../objects/stack-app.mdx#stackappuseproject) {/* THIS_LINE_PLATFORM react-like */} +. + +### Table of Contents + + + +--- + + + + + The unique ID of the project as a `string`. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + The display name of the project as a `string`. + + + + ```typescript + declare const displayName: string; + ``` + + + + + + + + The configuration settings for the project. + + ### Properties + + + Indicates if sign-up is enabled for the project. + + + + Specifies if credential-based authentication is enabled for the project. + + + + States whether magic link authentication is enabled for the project. + + + + Determines if client-side team creation is permitted within the project. + + + + Indicates if client-side user deletion is enabled for the project. + + + + + ```typescript + declare const config: { + signUpEnabled: boolean; + credentialEnabled: boolean; + magicLinkEnabled: boolean; + clientTeamCreationEnabled: boolean; + clientUserDeletionEnabled: boolean; + }; + ``` + + + diff --git a/docs/content/docs/sdk/types/team-permission.mdx b/docs/content/docs/sdk/types/team-permission.mdx new file mode 100644 index 0000000000..e39454a7b0 --- /dev/null +++ b/docs/content/docs/sdk/types/team-permission.mdx @@ -0,0 +1,30 @@ +--- +title: TeamPermission +full: true +--- + +The `TeamPermission` object represents a permission that a user has within a team. Currently, it contains only an `id` to specify the permission. + +You can get `TeamPermission` objects by calling functions such as `user.getPermission(...)` or `user.listPermissions()`. + +### Table of Contents + + + +--- + + + + + The identifier of the permission as a `string`. + + + + ```typescript + declare const id: string; + ``` + + + diff --git a/docs/content/docs/sdk/types/team-profile.mdx b/docs/content/docs/sdk/types/team-profile.mdx new file mode 100644 index 0000000000..0c6d5dfa85 --- /dev/null +++ b/docs/content/docs/sdk/types/team-profile.mdx @@ -0,0 +1,65 @@ +--- +title: TeamProfile +full: true +--- + +This is a detailed reference for the `TeamProfile` and `ServerTeamProfile` objects. + +On this page: +- [TeamProfile](#teamprofile) +- [ServerTeamProfile](#serverteamprofile) + +# `TeamProfile` + +The `TeamProfile` object represents the profile of a user within the context of a team. It includes the user's profile information specific to the team and can be accessed through the `teamUser.teamProfile` property on a `TeamUser` object. + +### Table of Contents + + + +--- + + + + + The display name of the user within the team context as a `string` or `null` if no display name is set. + + + + ```typescript + declare const displayName: string | null; + ``` + + + + + + + + The profile image URL of the user within the team context as a `string`, or `null` if no profile image is set. + + + + ```typescript + declare const profileImageUrl: string | null; + ``` + + + + +--- + +# `ServerTeamProfile` + +The `ServerTeamProfile` object is currently the same as `TeamProfile`. + +### Table of Contents + + + +--- diff --git a/docs/content/docs/sdk/types/team-user.mdx b/docs/content/docs/sdk/types/team-user.mdx new file mode 100644 index 0000000000..faf21917b0 --- /dev/null +++ b/docs/content/docs/sdk/types/team-user.mdx @@ -0,0 +1,90 @@ +--- +title: TeamUser +full: true +--- + +On this page: +- [TeamUser](#teamuser) +- [ServerTeamUser](#serverteamuser) + +--- + +# `TeamUser` + +The `TeamUser` object is used on the client side to represent a user in the context of a team, providing minimal information about the user, including their ID and team-specific profile. + +It is usually obtained by calling +`team.useUsers()` or {/* THIS_LINE_PLATFORM react-like */} +`team.listUsers()` on a [`Team` object](../types/team.mdx#team). + +### Table of Contents + + + +--- + + + + + The ID of the user. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + The team profile of the user as a `TeamProfile` object. + + + + ```typescript + declare const teamProfile: TeamProfile; + ``` + + + + +--- + +# `ServerTeamUser` + +The `ServerTeamUser` object is used on the server side to represent a user within a team. Besides the team profile, it also includes all the functionality of a [`ServerUser`](../types/user.mdx#serveruser). + +It is usually obtained by calling `serverTeam.listUsers()` on a [`ServerTeam` object](../types/team.mdx#serverteam). + +### Table of Contents + + + +--- + + + + + The team profile of the user as a `ServerTeamProfile` object. + + + + ```typescript + declare const teamProfile: ServerTeamProfile; + ``` + + + diff --git a/docs/content/docs/sdk/types/team.mdx b/docs/content/docs/sdk/types/team.mdx new file mode 100644 index 0000000000..372ba46c84 --- /dev/null +++ b/docs/content/docs/sdk/types/team.mdx @@ -0,0 +1,690 @@ +--- +title: Team +full: true +--- + +This is a detailed reference for the `Team` object. If you're looking for a more high-level overview, please refer to our [guide on teams](../../concepts/orgs-and-teams.mdx). + +On this page: +- [Team](#team) +- [ServerTeam](#serverteam) + +--- + +# `Team` + +A `Team` object contains basic information and functions about a team, to the extent of which a member of the team would have access to it. + +You can get `Team` objects with the +`user.useTeams()` or {/* THIS_LINE_PLATFORM react-like */} +`user.listTeams()` functions. The created team will then inherit the permissions of that user; for example, the `team.update(...)` function can only succeed if the user is allowed to make updates to the team. + +### Table of Contents + +; //$stack-link-to:#teamupdatedata + inviteUser(options): Promise; //$stack-link-to:#teaminviteuseroptions + listUsers(): Promise; //$stack-link-to:#teamlistusers + // NEXT_LINE_PLATFORM react-like + ⤷ useUsers(): TeamUser[]; //$stack-link-to:#teamuseusers + listInvitations(): Promise<{ ... }[]>; //$stack-link-to:#teamlistinvitations + // NEXT_LINE_PLATFORM react-like + ⤷ useInvitations(): { ... }[]; //$stack-link-to:#teamuseinvitations + + createApiKey(options): Promise; //$stack-link-to:#teamcreateapikeyoptions + listApiKeys(): Promise; //$stack-link-to:#teamlistapikeys + // NEXT_LINE_PLATFORM react-like + ⤷ useApiKeys(): TeamApiKey[]; //$stack-link-to:#teamuseapikeys +};`} /> + + + + + The team ID as a `string`. This value is always unique. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + The display name of the team as a `string`. + + + + ```typescript + declare const displayName: string; + ``` + + + + + + + + The profile image URL of the team as a `string`, or `null` if no profile image is set. + + + + ```typescript + declare const profileImageUrl: string | null; + ``` + + + + + + + + The client metadata of the team as a `Json` object. + + + + ```typescript + declare const clientMetadata: Json; + ``` + + + + + + + + The client read-only metadata of the team as a `Json` object. + + + + ```typescript + declare const clientReadOnlyMetadata: Json; + ``` + + + + + + + + Updates the team information. + + Note that this operation requires the current user to have the `$update_team` permission. If the user lacks this permission, an error will be thrown. + + ### Parameters + + + The fields to update. + + + The display name of the team. + + + + The profile image URL of the team. + + + + The client metadata of the team. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function update(options: { + displayName?: string; + profileImageUrl?: string | null; + clientMetadata?: Json; + }): Promise; + ``` + + + ```typescript Updating team details + await team.update({ + displayName: 'New Team Name', + profileImageUrl: 'https://example.com/profile.png', + clientMetadata: { + address: '123 Main St, Anytown, USA', + }, + }); + ``` + + + + + + + + + Sends an invitation email to a user to join the team. + + Note that this operation requires the current user to have the `$invite_members` permission. If the user lacks this permission, an error will be thrown. + + An invitation email containing a magic link will be sent to the specified user. If the user has an existing account, they will be automatically added to the team upon clicking the link. For users without an account, the link will guide them through the sign-up process before adding them to the team. + + ### Parameters + + + An object containing multiple properties. + + + The email of the user to invite. + + + + The URL where users will be redirected after accepting the team invitation. + + Required when calling `inviteUser()` in the server environment since the URL cannot be automatically determined. + + Example: `https://your-app-url.com/handler/team-invitation` + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function inviteUser(options: { + email: string; + callbackUrl?: string; + }): Promise; + ``` + + + ```typescript Sending a team invitation + await team.inviteUser({ + email: 'user@example.com', + }); + ``` + + + + + + + + + Gets a list of users in the team. + + Note that this operation requires the current user to have the `$read_members` permission. If the user lacks this permission, an error will be thrown. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function listUsers(): Promise; + ``` + + + ```typescript Listing team members + const users = await team.listUsers(); + users.forEach(user => { + console.log(user.id, user.teamProfile.displayName); + }); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Functionally equivalent to [`listUsers()`](#teamlistusers), but as a React hook. + + ### Parameters + + None. + + ### Returns + + `TeamUser[]` + + + + + ```typescript + declare function useUsers(): TeamUser[]; + ``` + + + ```typescript Listing team members in React component + const users = team.useUsers(); + users.forEach(user => { + console.log(user.id, user.teamProfile.displayName); + }); + ``` + + + + +{/* END_PLATFORM */} + + + + + Gets a list of invitations to the team. + + Note that this operation requires the current user to have the `$read_members` and `$invite_members` permissions. If the user lacks this permission, an error will be thrown. + + ### Parameters + + None. + + ### Returns + + `Promise<{ id: string, email: string, expiresAt: Date }[]>` + + + + + ```typescript + declare function listInvitations(): Promise<{ id: string, email: string, expiresAt: Date }[]>; + ``` + + + ```typescript Listing team invitations + const invitations = await team.listInvitations(); + invitations.forEach(invitation => { + console.log(invitation.id, invitation.email); + }); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Functionally equivalent to [`listInvitations()`](#teamlistinvitations), but as a React hook. + + ### Parameters + + None. + + ### Returns + + `{ id: string, email: string, expiresAt: Date }[]` + + + + + ```typescript + declare function useInvitations(): { id: string, email: string, expiresAt: Date }[]; + ``` + + + ```typescript Listing team invitations in React component + const invitations = team.useInvitations(); + invitations.forEach(invitation => { + console.log(invitation.id, invitation.email); + }); + ``` + + + + +{/* END_PLATFORM */} + + + + + Creates a new API key for the team. + + ### Parameters + + + An object containing multiple properties. + + + The name of the API key. + + + + The description of the API key. + + + + The expiration date of the API key. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function createApiKey(options: { + name: string; + description: string; + expiresAt: Date; + }): Promise; + ``` + + + ```typescript Creating a new API key + await team.createApiKey({ + name: 'New API Key', + description: 'This is a new API key', + expiresAt: new Date('2024-01-01'), + }); + ``` + + + + + + + + + Gets a list of API keys for the team. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function listApiKeys(): Promise; + ``` + + + ```typescript Listing API keys + const apiKeys = await team.listApiKeys(); + apiKeys.forEach(key => { + console.log(key.id, key.name); + }); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Functionally equivalent to [`listApiKeys()`](#teamlistapikeys), but as a React hook. + + ### Parameters + + None. + + ### Returns + + `TeamApiKey[]` + + + + + ```typescript + declare function useApiKeys(): TeamApiKey[]; + ``` + + + ```typescript Using API keys in React component + const apiKeys = team.useApiKeys(); + apiKeys.forEach(key => { + console.log(key.id, key.name); + }); + ``` + + + + +{/* END_PLATFORM */} + +--- + +# `ServerTeam` + +Like [`Team`](#team), but with [server permissions](../../concepts/stack-app.mdx#client-vs-server). Has full read and write access to everything. + +Calling `serverUser.getTeam(...)` and `serverUser.listTeams()` will return `ServerTeam` objects if the user is a [`ServerUser`](../types/user.mdx#serveruser). Alternatively, you can call `stackServerApp.getTeam('team_id_123')` or `stackServerApp.listTeams()` to query all teams of the project. + +`ServerTeam` extends the `Team` object, providing additional functions and properties as detailed below. It's important to note that while the `Team` object's functions may require specific user permissions, the corresponding functions in `ServerTeam` can be executed without these permission checks. This allows for more flexible and unrestricted team management on the server side. + +### Table of Contents + +; //$stack-link-to:#serverteamlistusers + // NEXT_LINE_PLATFORM react-like + ⤷ useUsers(): ServerTeamUser[]; //$stack-link-to:#serverteamuseusers + addUser(userId): Promise; //$stack-link-to:#serverteamadduseruserid + removeUser(userId): Promise; //$stack-link-to:#serverteamremoveuseruserid + delete(): Promise; //$stack-link-to:#serverteamdelete + };`} /> + + + + + The date and time when the team was created. + + + ```typescript + declare const createdAt: Date; + ``` + + + + + + + + The server metadata of the team as a `Json` object. + + + ```typescript + declare const serverMetadata: Json; + ``` + + + + + + + + Gets a list of users in the team. + + This is similar to the `listUsers` method on the `Team` object, but it returns `ServerTeamUser` objects instead of `TeamUser` objects and does not require any permissions. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function listUsers(): Promise; + ``` + + + ```typescript Listing server team members + const users = await team.listUsers(); + users.forEach(user => { + console.log(user.id, user.teamProfile.displayName); + }); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Functionally equivalent to [`listUsers()`](#serverteamlistusers), but as a React hook. + + ### Parameters + + None. + + ### Returns + + `ServerTeamUser[]` + + + + + ```typescript + declare function useUsers(): ServerTeamUser[]; + ``` + + + ```typescript Using server team members in React component + const users = team.useUsers(); + users.forEach(user => { + console.log(user.id, user.teamProfile.displayName); + }); + ``` + + + + +{/* END_PLATFORM */} + + + + + Adds a user to the team directly without sending an invitation email. + + ### Parameters + + + The ID of the user to add. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function addUser(userId: string): Promise; + ``` + + + ```typescript Adding a user to the team + await team.addUser('user_id_123'); + ``` + + + + + + + + + Removes a user from the team. + + ### Parameters + + + The ID of the user to remove. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function removeUser(userId: string): Promise; + ``` + + ### Examples + + ```typescript Removing a user from the team + await team.removeUser('user_id_123'); + ``` + + + + + + + + + Deletes the team. + + ### Parameters + + None. + + ### Returns + + `Promise` + + + + + ```typescript + declare function delete(): Promise; + ``` + + + ```typescript Deleting a team + await team.delete(); + ``` + + + + diff --git a/docs/content/docs/sdk/types/user.mdx b/docs/content/docs/sdk/types/user.mdx new file mode 100644 index 0000000000..0889748aa4 --- /dev/null +++ b/docs/content/docs/sdk/types/user.mdx @@ -0,0 +1,1577 @@ +--- +title: User +full: true +--- + +This is a detailed reference for the `User` object. If you're looking for a more high-level overview, please refer to our guide on users [here](../../getting-started/users.mdx). + +On this page: +- [CurrentUser](#currentuser) +- [ServerUser](#serveruser) +- [CurrentServerUser](#currentserveruser) + +# `CurrentUser` + +You can call `useUser()` or `stackServerApp.getUser()` to get the `CurrentUser` object. + +### Table of Contents + +; //$stack-link-to:#currentuserupdate + updatePassword(data): Promise; //$stack-link-to:#currentuserupdatepassword + getAuthHeaders(): Promise>; //$stack-link-to:#currentusergetauthheaders + getAuthJson(): Promise<{ accessToken: string | null }>; //$stack-link-to:#currentusergetauthjson + signOut([options]): Promise; //$stack-link-to:#currentusersignout + delete(): Promise; //$stack-link-to:#currentuserdelete + + getTeam(id): Promise; //$stack-link-to:#currentusergetteam + // NEXT_LINE_PLATFORM react-like + ⤷ useTeam(id): Team | null; //$stack-link-to:#currentuseruseteam + listTeams(): Promise; //$stack-link-to:#currentuserlistteams + // NEXT_LINE_PLATFORM react-like + ⤷ useTeams(): Team[]; //$stack-link-to:#currentuseruseteams + setSelectedTeam(team): Promise; //$stack-link-to:#currentusersetselectedteam + createTeam(data): Promise; //$stack-link-to:#currentusercreateteam + leaveTeam(team): Promise; //$stack-link-to:#currentuserleaveteam + getTeamProfile(team): Promise; //$stack-link-to:#currentusergetteamprofile + // NEXT_LINE_PLATFORM react-like + ⤷ useTeamProfile(team): EditableTeamMemberProfile; //$stack-link-to:#currentuseruseteamprofile + + hasPermission(scope, permissionId): Promise; //$stack-link-to:#currentuserhaspermission + getPermission(scope, permissionId[, options]): Promise; //$stack-link-to:#currentusergetpermission + // NEXT_LINE_PLATFORM react-like + ⤷ usePermission(scope, permissionId[, options]): TeamPermission | null; //$stack-link-to:#currentuserusepermission + listPermissions(scope[, options]): Promise; //$stack-link-to:#currentuserlistpermissions + // NEXT_LINE_PLATFORM react-like + ⤷ usePermissions(scope[, options]): TeamPermission[]; //$stack-link-to:#currentuserusepermissions + + listContactChannels(): Promise; //$stack-link-to:#currentuserlistcontactchannels + // NEXT_LINE_PLATFORM react-like + ⤷ useContactChannels(): ContactChannel[]; //$stack-link-to:#currentuserusecontactchannels + + createApiKey(options): Promise; //$stack-link-to:#currentusercreateapikey + listApiKeys(): Promise; //$stack-link-to:#currentuserlistapikeys + // NEXT_LINE_PLATFORM react-like + ⤷ useApiKeys(): UserApiKey[]; //$stack-link-to:#currentuseruseapikeys +};`} /> + + + + + The user ID as a `string`. This is the unique identifier of the user. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + The display name of the user as a `string` or `null` if not set. The user can modify this value. + + + + ```typescript + declare const displayName: string | null; + ``` + + + + + + + + The primary email of the user as a `string` or `null`. Note that this is not necessarily unique. + + + + ```typescript + declare const primaryEmail: string | null; + ``` + + + + + + + + A `boolean` indicating whether the primary email of the user is verified. + + + + ```typescript + declare const primaryEmailVerified: boolean; + ``` + + + + + + + + The profile image URL of the user as a `string` or `null` if no profile image is set. + + + + ```typescript + declare const profileImageUrl: string | null; + ``` + + + + + + + + The date and time when the user signed up, as a `Date`. + + + + ```typescript + declare const signedUpAt: Date; + ``` + + + + + + + + A `boolean` indicating whether the user has a password set. + + + + ```typescript + declare const hasPassword: boolean; + ``` + + + + + + + + The client metadata of the user as an `object`. This metadata is visible on the client side but should not contain sensitive or server-only information. + + + + ```typescript + declare const clientMetadata: Json; + ``` + + + + + + + + Read-only metadata visible on the client side. This metadata can only be modified on the server side. + + + + ```typescript + declare const clientReadOnlyMetadata: Json; + ``` + + + + + + + + The currently selected team for the user, if applicable, as a `Team` object or `null` if no team is selected. + + + + ```typescript + declare const selectedTeam: Team | null; + ``` + + + + + + + + Updates the user information. + + ### Parameters + + + The fields to update. + + + The new display name for the user. + + + Custom metadata visible to the client. + + + The ID of the team to set as selected, or `null` to clear selection. + + + The URL of the user's new profile image, or `null` to remove it. + + + + + ### Returns + + `Promise` + + + + ```typescript + declare function update(data: { + displayName?: string; + clientMetadata?: Json; + selectedTeamId?: string | null; + profileImageUrl?: string | null; + }): Promise; + ``` + + + ```typescript Updating user details + await user.update({ + displayName: "New Display Name", + clientMetadata: { + address: "123 Main St", + }, + }); + ``` + + + + + + + + + Gets the team with the specified ID. + + ### Parameters + + + The ID of the team to get. + + + ### Returns + + `Promise`: The team object, or `null` if the team is not found or the user is not a member of the team. + + + + + ```typescript + declare function getTeam(id: string): Promise; + ``` + + + ```typescript Getting a team by ID + const team = await user.getTeam("teamId"); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Gets the team with the given ID. This is the same as `getTeam` but is used as a React hook. + + ### Parameters + + + The ID of the team to get. + + + ### Returns + + `Team | null`: The team object, or `null` if the team is not found or the user is not a member of the team. + + + + ```typescript + declare function useTeam(id: string): Team | null; + ``` + + + + ```typescript Using a team in a React component + const team = user.useTeam("teamId"); + ``` + + + + +{/* END_PLATFORM */} + + + + + Lists all the teams the user is a member of. + + ### Parameters + + None. + + ### Returns + + `Promise`: The list of teams. + + + + + ```typescript + declare function listTeams(): Promise; + ``` + + + ```typescript Listing all teams + const teams = await user.listTeams(); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Lists all the teams the user is a member of. This is the same as `listTeams` but is used as a React hook. + + ### Parameters + + None. + + ### Returns + + `Team[]`: The list of teams. + + + + + ```typescript + declare function useTeams(): Team[]; + ``` + + + ```typescript Using teams in a React component + const teams = user.useTeams(); + ``` + + + + +{/* END_PLATFORM */} + + + + + Sets the currently selected team for the user. + + ### Parameters + + + The team to set as selected, or `null` to clear selection. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function setSelectedTeam(team: Team | null): Promise; + ``` + + + ```typescript Setting the selected team + const team = await user.getTeam("team_id_123"); + await user.setSelectedTeam(team); + ``` + + + + + + + + + Creates a new team for the user. The user will be added to the team and given creator permissions. + + **Note**: If client-side team creation is disabled in the Stack dashboard, this will throw an error. + + ### Parameters + + + The data for creating the team. + + + The display name for the team. + + + The URL of the team's profile image, or `null` to remove it. + + + + + ### Returns + + `Promise`: The created team. + + + + + ```typescript + declare function createTeam(data: { + displayName: string; + profileImageUrl?: string | null; + }): Promise; + ``` + + + ```typescript Creating a new team + const team = await user.createTeam({ + displayName: "New Team", + profileImageUrl: "https://example.com/profile.jpg", + }); + ``` + + + + + + + + + Allows the user to leave a team. If the user is not a member of the team, this will throw an error. + + ### Parameters + + + The team to leave. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function leaveTeam(team: Team): Promise; + ``` + + + ```typescript Leaving a team + await user.leaveTeam(team); + ``` + + + + + + + + + Retrieves the user's profile within a specific team. + + ### Parameters + + + The team to retrieve the profile for. + + + ### Returns + + `Promise`: The user's editable profile for the specified team. + + + + + ```typescript + declare function getTeamProfile(team: Team): Promise; + ``` + + + ```typescript Getting a team profile + const profile = await user.getTeamProfile(team); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Retrieves the user's profile within a specific team. This is the same as `getTeamProfile` but is used as a React hook. + + ### Parameters + + + The team to retrieve the profile for. + + + ### Returns + + `EditableTeamMemberProfile`: The user's editable profile for the specified team. + + + + + ```typescript + declare function useTeamProfile(team: Team): EditableTeamMemberProfile; + ``` + + + ```typescript Using a team profile in React + const profile = user.useTeamProfile(team); + ``` + + + + +{/* END_PLATFORM */} + + + + + Checks if the user has a specific permission for a team. + + ### Parameters + + + The team to check the permission for. + + + The ID of the permission to check. + + + ### Returns + + `Promise`: Whether the user has the specified permission. + + + + + ```typescript + declare function hasPermission(scope: Team, permissionId: string): Promise; + ``` + + + ```typescript Checking user permission + const hasPermission = await user.hasPermission(team, "permissionId"); + ``` + + + + + + + + + Retrieves a specific permission for a user within a team. + + ### Parameters + + + The team to retrieve the permission for. + + + The ID of the permission to retrieve. + + + An object containing multiple properties. + + + Whether to retrieve the permission recursively. Default is `true`. + + + + + ### Returns + + `Promise`: The permission object, or `null` if not found. + + + + + ```typescript + declare function getPermission(scope: Team, permissionId: string, options?: { recursive?: boolean }): Promise; + ``` + + + ```typescript Getting a permission + const permission = await user.getPermission(team, "read_secret_info"); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Retrieves a specific permission for a user within a team, used as a React hook. + + ### Parameters + + + The team to retrieve the permission for. + + + The ID of the permission to retrieve. + + + An object containing multiple properties. + + + Whether to retrieve the permission recursively. Default is `true`. + + + + + ### Returns + + `TeamPermission | null`: The permission object, or `null` if not found. + + + + + ```typescript + declare function usePermission(scope: Team, permissionId: string, options?: { recursive?: boolean }): TeamPermission | null; + ``` + + + ```typescript Using a permission in React + const permission = user.usePermission(team, "read_secret_info"); + ``` + + + + +{/* END_PLATFORM */} + + + + + Lists all permissions the user has for a specified team. + + ### Parameters + + + The team to list permissions for. + + + An object containing multiple properties. + + + Whether to list the permissions recursively. Default is `true`. + + + + + ### Returns + + `Promise`: An array of permissions. + + + + + ```typescript + declare function listPermissions(scope: Team, options?: { recursive?: boolean }): Promise; + ``` + + + ```typescript Listing user permissions + const permissions = await user.listPermissions(team); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Lists all permissions the user has for a specified team, used as a React hook. + + ### Parameters + + + The team to retrieve permissions for. + + + An object containing multiple properties. + + + Whether to list the permissions recursively. Default is `true`. + + + + + ### Returns + + `TeamPermission[]`: An array of permissions. + + + + + ```typescript + declare function usePermissions(scope: Team, options?: { recursive?: boolean }): TeamPermission[]; + ``` + + + ```typescript Using permissions in a React component + const permissions = user.usePermissions(team); + ``` + + + + +{/* END_PLATFORM */} + + + + + Lists all the contact channels of the user. + + ### Parameters + + No parameters. + + ### Returns + + `Promise`: An array of contact channels. + + + + + ```typescript + declare function listContactChannels(): Promise; + ``` + + + ```typescript Listing contact channels + const contactChannels = await user.listContactChannels(); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Lists all the contact channels of the user, used as a React hook. + + ### Parameters + + No parameters. + + ### Returns + + `ContactChannel[]`: An array of contact channels. + + + + + ```typescript + declare function useContactChannels(): ContactChannel[]; + ``` + + + ```typescript Using contact channels in React + const contactChannels = user.useContactChannels(); + ``` + + + + +{/* END_PLATFORM */} + + + + + Creates a new API key for the user, which can be used for programmatic access to your application's backend. + + ### Parameters + + + Options for creating the API key. + + + A human-readable description of the API key's purpose. + + + The date when the API key will expire. Use null for keys that don't expire. + + + Whether the API key is public. Defaults to false. + + - **Secret API Keys** (default) begin with `sk_` and are monitored by Stack Auth's secret scanner, which can revoke them if detected in public code repositories. + - **Public API Keys** begin with `pk_` and are designed for client-side code where exposure is not a concern. + + + + + ### Returns + + `Promise`: The newly created API key. Note that this is the only time the full API key value will be visible. + + + + + ```typescript + declare function createApiKey(options: { + description: string; + expiresAt: Date | null; + isPublic?: boolean; + }): Promise; + ``` + + + ```typescript Creating an API key + const apiKey = await user.createApiKey({ + description: "Backend integration", + expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days + isPublic: false, + }); + + // Save the API key value securely, as it won't be retrievable later + console.log("API Key:", apiKey.value); + ``` + + + + + + + + + Lists all API keys that belong to the user. + + ### Parameters + + None. + + ### Returns + + `Promise`: An array of API keys belonging to the user. + + + + + ```typescript + declare function listApiKeys(): Promise; + ``` + + + ```typescript Listing API keys + const apiKeys = await user.listApiKeys(); + console.log(`You have ${apiKeys.length} API keys`); + + // Find keys that are about to expire + const soonToExpire = apiKeys.filter(key => + key.expiresAt && key.expiresAt.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1000 + ); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Lists all API keys that belong to the user, used as a React hook. + + ### Parameters + + None. + + ### Returns + + `UserApiKey[]`: An array of API keys belonging to the user. + + + + + ```typescript + declare function useApiKeys(): UserApiKey[]; + ``` + + + ```typescript Using API keys in a React component + function ApiKeysList() { + const user = useUser(); + const apiKeys = user.useApiKeys(); + + return ( +
    +

    Your API Keys ({apiKeys.length})

    +
      + {apiKeys.map(key => ( +
    • + {key.description} - Last four: {key.value.lastFour} + {key.isValid() ? ' (valid)' : ` (invalid: ${key.whyInvalid()})`} +
    • + ))} +
    +
    + ); + } + ``` +
    +
    +
    +
    +{/* END_PLATFORM */} + + + + + Updates the user's password. + + ### Parameters + + + The fields required for updating the password. + + + The current password of the user. + + + The new password for the user. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function updatePassword(data: { + oldPassword: string; + newPassword: string; + }): Promise; + ``` + + + ```typescript Updating user password + const result = await user.updatePassword({ + oldPassword: "currentPassword", + newPassword: "newPassword", + }); + if (result.status === "error") { + console.error("Error updating password", result.error); + } else { + console.log("Password updated"); + } + ``` + + + + + + + + + Returns headers for sending authenticated HTTP requests to external servers. Most commonly used in cross-origin + requests. Similar to `getAuthJson`, but specifically for HTTP requests. + + If you are using `tokenStore: "cookie"`, you don't need this for same-origin requests. However, most + browsers now disable third-party cookies by default, so we must pass authentication tokens by header instead + if the client and server are on different origins. + + This function returns a header object that can be used with `fetch` or other HTTP request libraries to send + authenticated requests. + + On the server, you can then pass in the `Request` object to the `tokenStore` option + of your Stack app. Please note that CORS does not allow most headers by default, so you + must include `x-stack-auth` in the [`Access-Control-Allow-Headers` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) + of the CORS preflight response. + + ### Parameters + + No parameters. + + ### Returns + + `Promise>`: An object containing the authentication headers. + + + + + ```typescript + declare function getAuthHeaders(): Promise>; + ``` + + + ```typescript Passing auth headers to an external server + // client + const res = await fetch("https://api.example.com", { + headers: { + ...await stackApp.getAuthHeaders() + // you can also add your own headers here + }, + }); + + // server + function handleRequest(req: Request) { + const user = await stackServerApp.getUser({ tokenStore: req }); + return new Response("Welcome, " + user.displayName); + } + ``` + + + + + + + + + Creates a JSON-serializable object containing the information to authenticate a user on an external server. + + While `getAuthHeaders` is the recommended way to send authentication tokens over HTTP, your app may use + a different protocol, for example WebSockets or gRPC. This function returns a token object that can be JSON-serialized and sent to the server in any way you like. + + On the server, you can pass in this token object into the `tokenStore` option to fetch user details. + + ### Parameters + + No parameters. + + ### Returns + + `Promise<{ accessToken: string | null }>`: An object containing the access token. + + + + + ```typescript + declare function getAuthJson(): Promise<{ accessToken: string | null }>; + ``` + + + ```typescript Passing auth tokens over an RPC call + // client + const res = await rpcCall(rpcEndpoint, { + data: { + auth: await stackApp.getAuthJson(), + }, + }); + + // server + function handleRequest(data) { + const user = await stackServerApp.getUser({ tokenStore: data.auth }); + return new Response("Welcome, " + user.displayName); + } + ``` + + + + + + + + + Signs out the user and clears the session. + + ### Parameters + + + An object containing multiple properties. + + + The URL to redirect to after signing out. Defaults to the `afterSignOut` URL from the Stack app's `urls` object. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function signOut(options?: { redirectUrl?: string }): Promise; + ``` + + + ```typescript Signing out + await user.signOut(); + ``` + + + + + + + + + Deletes the user. This action is irreversible and can only be used if client-side user deletion is enabled in the Stack dashboard. + + ### Parameters + + No parameters. + + ### Returns + + `Promise` + + + + + ```typescript + declare function delete(): Promise; + ``` + + + ```typescript Deleting the user + await user.delete(); + ``` + + + + + +--- + +
    + +# `ServerUser` + +The `ServerUser` object contains most `CurrentUser` properties and methods with the exception of those that require an active session (`getAuthJson` and `signOut`). It also contains some additional functions that require [server-level permissions](../../concepts/stack-app.mdx#client-vs-server). + +### Table of Contents + + //$stack-link-to:#currentuser + & { + lastActiveAt: Date; //$stack-link-to:#serveruserlastactiveat + serverMetadata: Json; //$stack-link-to:#serveruserservermetadata + + update(data): Promise; //$stack-link-to:#serveruserupdate + + listContactChannels(): Promise; //$stack-link-to:#serveruserlistcontactchannels + // NEXT_LINE_PLATFORM react-like + ⤷ useContactChannels(): ContactChannel[]; //$stack-link-to:#serveruserusecontactchannels + + grantPermission(scope, permissionId): Promise; //$stack-link-to:#serverusergrantpermission + revokePermission(scope, permissionId): Promise; //$stack-link-to:#serveruserrevokepermission + };`} /> + + + + + The last active date and time of the user as a `Date`. + + + + ```typescript + declare const lastActiveAt: Date; + ``` + + + + + + + + The server metadata of the user, accessible only on the server side. + + + + ```typescript + declare const serverMetadata: Json; + ``` + + + + + + + + Updates the user's information on the server side. This is similar to the `CurrentUser.update()` method but includes additional capabilities, such as updating server metadata or setting a new password directly. + + ### Parameters + + + The fields to update. + + + The new display name for the user. + + + The new primary email for the user. + + + Whether the primary email is verified. + + + Whether auth should be enabled for the primary email. + + + The new password for the user. + + + The ID of the team to set as selected, or `null` to clear selection. + + + The URL of the user's new profile image, or `null` to remove. + + + Metadata visible on the client side. + + + Metadata that is read-only on the client but modifiable on the server side. + + + Metadata only accessible and modifiable on the server side. + + + + + ### Returns + + `Promise` + + + + + ```typescript + declare function update(data: { + displayName?: string; + profileImageUrl?: string | null; + primaryEmail?: string, + primaryEmailVerified?: boolean, + primaryEmailAuthEnabled?: boolean, + password?: string; + selectedTeamId?: string | null; + clientMetadata?: Json; + clientReadOnlyMetadata?: Json; + serverMetadata?: Json; + }): Promise; + ``` + + + ```typescript Updating user details on the server + await serverUser.update({ + displayName: "Updated Display Name", + password: "newSecurePassword", + serverMetadata: { + internalNote: "Confidential information", + }, + }); + ``` + + + + + + + + + Lists all the contact channels of the user on the server side. This is similar to `CurrentUser.listContactChannels()` but returns a list of `ServerContactChannel` objects, which may include additional server-only details. + + ### Parameters + + No parameters. + + ### Returns + + `Promise`: An array of server-specific contact channels. + + + + + ```typescript + declare function listContactChannels(): Promise; + ``` + + + ```typescript Listing server-specific contact channels + const contactChannels = await serverUser.listContactChannels(); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + + + Functionally equivalent to [`listContactChannels()`](#serveruserlistcontactchannels), but as a React hook. + + + + ```typescript + declare function useContactChannels(): ContactChannel[]; + ``` + + + +{/* END_PLATFORM */} + + + + + Grants a specific permission to the user for a given team. + + ### Parameters + + + The team to grant the permission for. + + + The ID of the permission to grant. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function grantPermission(scope: Team, permissionId: string): Promise; + ``` + + + ```typescript Granting permission to a user + await serverUser.grantPermission(team, "read_secret_info"); + ``` + + + + + + + + + Revokes a specific permission from the user for a given team. + + ### Parameters + + + The team to revoke the permission from. + + + The ID of the permission to revoke. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function revokePermission(scope: Team, permissionId: string): Promise; + ``` + + + ```typescript Revoking permission from a user + await serverUser.revokePermission(team, "read_secret_info"); + ``` + + + + + + + + + Creates a new API key for the user, which can be used for programmatic access to your application's backend. + + ### Parameters + + + Options for creating the API key. + + + A human-readable description of the API key's purpose. + + + The date when the API key will expire. Use null for keys that don't expire. + + + Whether the API key is public. Defaults to false. + + - **Secret API Keys** (default) begin with `sk_` and are monitored by Stack Auth's secret scanner, which can revoke them if detected in public code repositories. + - **Public API Keys** begin with `pk_` and are designed for client-side code where exposure is not a concern. + + + + + ### Returns + + `Promise`: The newly created API key. Note that this is the only time the full API key value will be visible. + + + + + ```typescript + declare function createApiKey(options: { + description: string; + expiresAt: Date | null; + isPublic?: boolean; + }): Promise; + ``` + + + ```typescript Creating an API key + const apiKey = await serverUser.createApiKey({ + description: "Backend integration", + expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days + isPublic: false, + }); + + // Save the API key value securely, as it won't be retrievable later + console.log("API Key:", apiKey.value); + ``` + + + + + + + + + Lists all API keys that belong to the user. + + ### Parameters + + None. + + ### Returns + + `Promise`: An array of API keys belonging to the user. + + + + + ```typescript + declare function listApiKeys(): Promise; + ``` + + + ```typescript Listing API keys + const apiKeys = await serverUser.listApiKeys(); + console.log(`You have ${apiKeys.length} API keys`); + + // Find keys that are about to expire + const soonToExpire = apiKeys.filter(key => + key.expiresAt && key.expiresAt.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1000 + ); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Lists all API keys that belong to the user, used as a React hook. + + ### Parameters + + None. + + ### Returns + + `UserApiKey[]`: An array of API keys belonging to the user. + + + + + ```typescript + declare function useApiKeys(): UserApiKey[]; + ``` + + + ```typescript Using API keys in a React component + function ApiKeysList() { + const user = useUser(); + const apiKeys = user.useApiKeys(); + + return ( +
    +

    Your API Keys ({apiKeys.length})

    +
      + {apiKeys.map(key => ( +
    • + {key.description} - Last four: {key.value.lastFour} + {key.isValid() ? ' (valid)' : ` (invalid: ${key.whyInvalid()})`} +
    • + ))} +
    +
    + ); + } + ``` +
    +
    +
    +
    +{/* END_PLATFORM */} + +--- + +
    + +# `CurrentServerUser` + +The `CurrentServerUser` object combines all the properties and methods of both `CurrentUser` and `ServerUser`. You can obtain a `CurrentServerUser` by calling `stackServerApp.getUser()` on the server side. + +### Table of Contents + + From 318385da7f2980473f684e79f28c4a7f96f66620 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 1 Oct 2025 09:52:55 -0500 Subject: [PATCH 04/55] update new codeblock --- .../platform-codeblock-example.mdx | 614 ++++++++++++++++++ docs/content/docs/(guides)/meta.json | 1 + .../mdx/platform-codeblock-sync-demo.tsx | 103 +++ .../src/components/mdx/platform-codeblock.tsx | 425 ++++++++++++ docs/src/components/mdx/platform-config.ts | 159 +++++ .../mdx/simple-platform-codeblock.tsx | 102 +++ .../mdx/simple-platform-example.mdx | 120 ++++ docs/src/mdx-components.tsx | 4 + 8 files changed, 1528 insertions(+) create mode 100644 docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx create mode 100644 docs/src/components/mdx/platform-codeblock-sync-demo.tsx create mode 100644 docs/src/components/mdx/platform-codeblock.tsx create mode 100644 docs/src/components/mdx/platform-config.ts create mode 100644 docs/src/components/mdx/simple-platform-codeblock.tsx create mode 100644 docs/src/components/mdx/simple-platform-example.mdx diff --git a/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx b/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx new file mode 100644 index 0000000000..c9cb0e1599 --- /dev/null +++ b/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx @@ -0,0 +1,614 @@ +--- +title: "Platform Codeblock Example" +--- + +# Platform Codeblock Example + +Here's how to use the new PlatformCodeblock component: + + bool: + # Your user validation logic here + return True # Placeholder + +def create_jwt_token(username: str) -> str: + # Your JWT token creation logic here + return "your-jwt-token" # Placeholder`, + language: "python" + }, + "Flask": { + code: `from flask import Flask, request, jsonify +from werkzeug.security import check_password_hash +from flask_jwt_extended import JWTManager, create_access_token + +app = Flask(__name__) +app.config['JWT_SECRET_KEY'] = 'your-secret-key' +jwt = JWTManager(app) + +@app.route('/login', methods=['POST']) +def login(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + + # Validate user credentials (implement your logic) + user = get_user_by_username(username) + if user and check_password_hash(user.password_hash, password): + access_token = create_access_token(identity=username) + return jsonify({ + 'access_token': access_token, + 'success': True + }) + + return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 + +def get_user_by_username(username): + # Your user lookup logic here + pass # Placeholder`, + language: "python" + } + }, + "JavaScript": { + "Next.js": { + code: `import { NextRequest, NextResponse } from 'next/server'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; + +export async function POST(request: NextRequest) { + try { + const { username, password } = await request.json(); + + // Validate user credentials (implement your logic) + const user = await getUserByUsername(username); + if (!user) { + return NextResponse.json( + { success: false, error: 'Invalid credentials' }, + { status: 401 } + ); + } + + const isValidPassword = await bcrypt.compare(password, user.passwordHash); + if (!isValidPassword) { + return NextResponse.json( + { success: false, error: 'Invalid credentials' }, + { status: 401 } + ); + } + + // Create JWT token + const token = jwt.sign( + { userId: user.id, username: user.username }, + process.env.JWT_SECRET!, + { expiresIn: '24h' } + ); + + return NextResponse.json({ + success: true, + token, + user: { id: user.id, username: user.username } + }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Internal server error' }, + { status: 500 } + ); + } +} + +async function getUserByUsername(username: string) { + // Your user lookup logic here + return null; // Placeholder +}`, + language: "typescript" + }, + "Express": { + code: `const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const app = express(); + +app.use(express.json()); + +app.post('/login', async (req, res) => { + try { + const { username, password } = req.body; + + // Validate user credentials (implement your logic) + const user = await getUserByUsername(username); + if (!user) { + return res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } + + const isValidPassword = await bcrypt.compare(password, user.passwordHash); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } + + // Create JWT token + const token = jwt.sign( + { userId: user.id, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + res.json({ + success: true, + token, + user: { id: user.id, username: user.username } + }); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +async function getUserByUsername(username) { + // Your user lookup logic here + return null; // Placeholder +} + +app.listen(3000, () => { + console.log('Server running on port 3000'); +});`, + language: "javascript" + }, + "React": { + code: `import React, { useState } from 'react'; +import axios from 'axios'; + +interface LoginFormProps { + onLoginSuccess: (token: string, user: any) => void; +} + +export function LoginForm({ onLoginSuccess }: LoginFormProps) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await axios.post('/api/login', { + username, + password + }); + + if (response.data.success) { + onLoginSuccess(response.data.token, response.data.user); + } else { + setError(response.data.error || 'Login failed'); + } + } catch (err: any) { + setError(err.response?.data?.error || 'Network error'); + } finally { + setLoading(false); + } + }; + + return ( +
    +
    + + setUsername(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" + required + /> +
    + +
    + + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" + required + /> +
    + + {error && ( +
    {error}
    + )} + + +
    + ); +}`, + language: "typescript" + } + } + }} +/> + + + bool: + # Your user validation logic here + return True # Placeholder + +def create_jwt_token(username: str) -> str: + # Your JWT token creation logic here + return "your-jwt-token" # Placeholder`, + language: "python" + }, + "Flask": { + code: `from flask import Flask, request, jsonify +from werkzeug.security import check_password_hash +from flask_jwt_extended import JWTManager, create_access_token + +app = Flask(__name__) +app.config['JWT_SECRET_KEY'] = 'your-secret-key' +jwt = JWTManager(app) + +@app.route('/login', methods=['POST']) +def login(): + data = request.get_json() + username = data.get('username') + password = data.get('password') + + # Validate user credentials (implement your logic) + user = get_user_by_username(username) + if user and check_password_hash(user.password_hash, password): + access_token = create_access_token(identity=username) + return jsonify({ + 'access_token': access_token, + 'success': True + }) + + return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 + +def get_user_by_username(username): + # Your user lookup logic here + pass # Placeholder`, + language: "python" + } + }, + "JavaScript": { + "Next.js": { + code: `import { NextRequest, NextResponse } from 'next/server'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; + +export async function POST(request: NextRequest) { + try { + const { username, password } = await request.json(); + + // Validate user credentials (implement your logic) + const user = await getUserByUsername(username); + if (!user) { + return NextResponse.json( + { success: false, error: 'Invalid credentials' }, + { status: 401 } + ); + } + + const isValidPassword = await bcrypt.compare(password, user.passwordHash); + if (!isValidPassword) { + return NextResponse.json( + { success: false, error: 'Invalid credentials' }, + { status: 401 } + ); + } + + // Create JWT token + const token = jwt.sign( + { userId: user.id, username: user.username }, + process.env.JWT_SECRET!, + { expiresIn: '24h' } + ); + + return NextResponse.json({ + success: true, + token, + user: { id: user.id, username: user.username } + }); + } catch (error) { + return NextResponse.json( + { success: false, error: 'Internal server error' }, + { status: 500 } + ); + } +} + +async function getUserByUsername(username: string) { + // Your user lookup logic here + return null; // Placeholder +}`, + language: "typescript" + }, + "Express": { + code: `const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const app = express(); + +app.use(express.json()); + +app.post('/login', async (req, res) => { + try { + const { username, password } = req.body; + + // Validate user credentials (implement your logic) + const user = await getUserByUsername(username); + if (!user) { + return res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } + + const isValidPassword = await bcrypt.compare(password, user.passwordHash); + if (!isValidPassword) { + return res.status(401).json({ + success: false, + error: 'Invalid credentials' + }); + } + + // Create JWT token + const token = jwt.sign( + { userId: user.id, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + res.json({ + success: true, + token, + user: { id: user.id, username: user.username } + }); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +async function getUserByUsername(username) { + // Your user lookup logic here + return null; // Placeholder +} + +app.listen(3000, () => { + console.log('Server running on port 3000'); +});`, + language: "javascript" + }, + "React": { + code: `import React, { useState } from 'react'; +import axios from 'axios'; + +interface LoginFormProps { + onLoginSuccess: (token: string, user: any) => void; +} + +export function LoginForm({ onLoginSuccess }: LoginFormProps) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await axios.post('/api/login', { + username, + password + }); + + if (response.data.success) { + onLoginSuccess(response.data.token, response.data.user); + } else { + setError(response.data.error || 'Login failed'); + } + } catch (err: any) { + setError(err.response?.data?.error || 'Network error'); + } finally { + setLoading(false); + } + }; + + return ( +
    +
    + + setUsername(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" + required + /> +
    + +
    + + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" + required + /> +
    + + {error && ( +
    {error}
    + )} + + +
    + ); +}`, + language: "typescript" + } + } + }} +/> + +## Usage + +The PlatformCodeblock component accepts the following props: + +- `platforms`: An object defining the platforms and their frameworks with code examples +- `defaultPlatform`: The platform to show by default +- `defaultFrameworks`: Default framework for each platform +- `title`: Optional title for the code block +- `className`: Additional CSS classes + +The component will automatically: +- Show a platform selector on the left side +- Show framework tabs on the right side for the selected platform +- Syntax highlight the code based on the specified language +- Adapt to light/dark theme changes +- Persist selections as users switch between platforms and frameworks diff --git a/docs/content/docs/(guides)/meta.json b/docs/content/docs/(guides)/meta.json index 1767574811..53086f03ef 100644 --- a/docs/content/docs/(guides)/meta.json +++ b/docs/content/docs/(guides)/meta.json @@ -10,6 +10,7 @@ "getting-started/components", "getting-started/users", "getting-started/production", + "getting-started/platform-codeblock-example", "---Concepts---", "concepts/api-keys", "concepts/backend-integration", diff --git a/docs/src/components/mdx/platform-codeblock-sync-demo.tsx b/docs/src/components/mdx/platform-codeblock-sync-demo.tsx new file mode 100644 index 0000000000..d50fa3deee --- /dev/null +++ b/docs/src/components/mdx/platform-codeblock-sync-demo.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { PlatformCodeblock } from './platform-codeblock'; + +// Demo component to test multi-instance synchronization +export function PlatformCodeblockSyncDemo() { + const samplePlatforms = { + "Python": { + "Django": { + code: `# Django Authentication +from django.contrib.auth import authenticate, login +from django.http import JsonResponse + +def login_view(request): + user = authenticate(username='user', password='pass') + if user: + login(request, user) + return JsonResponse({'success': True})`, + language: "python", + filename: "views.py" + }, + "FastAPI": { + code: `# FastAPI Authentication +from fastapi import FastAPI, HTTPException + +app = FastAPI() + +@app.post("/login") +async def login(): + # Authentication logic here + return {"access_token": "token", "token_type": "bearer"}`, + language: "python", + filename: "main.py" + } + }, + "JavaScript": { + "Next.js": { + code: `// Next.js API Route +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + const { username, password } = await request.json(); + + // Authentication logic here + return NextResponse.json({ success: true }); +}`, + language: "typescript", + filename: "app/api/login/route.ts" + }, + "Express": { + code: `// Express.js Route +const express = require('express'); +const app = express(); + +app.post('/login', async (req, res) => { + const { username, password } = req.body; + + // Authentication logic here + res.json({ success: true }); +});`, + language: "javascript", + filename: "server.js" + } + } + }; + + return ( +
    +
    +

    First Instance

    + +
    + +
    +

    Second Instance

    + +
    + +
    +

    + Test Single Cascading Dropdown: Click "Change" to open the dropdown, select a platform, then select a framework within that platform! +

    +

    + Navigation: Use the "Back" button to return to platform selection from framework selection! +

    +

    + Cross-Instance Sync: Change selections in one instance and watch both instances update simultaneously! +

    +

    + Persistence: Refresh the page and your selections will be remembered across sessions! +

    +
    +
    + ); +} diff --git a/docs/src/components/mdx/platform-codeblock.tsx b/docs/src/components/mdx/platform-codeblock.tsx new file mode 100644 index 0000000000..6c052b39c6 --- /dev/null +++ b/docs/src/components/mdx/platform-codeblock.tsx @@ -0,0 +1,425 @@ +'use client'; + +import { ChevronDown } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { codeToHtml } from 'shiki'; +import { cn } from '../../lib/cn'; + +// Global state management for platform and framework selection +type PlatformChangeListener = (platform: string) => void; +type FrameworkChangeListener = (platform: string, framework: string) => void; + +const platformListeners = new Map(); +const frameworkListeners = new Map(); + +let globalSelectedPlatform: string | null = null; +let globalSelectedFrameworks: { [platform: string]: string } = {}; + +function addPlatformListener(id: string, listener: PlatformChangeListener): void { + const list = platformListeners.get(id) ?? []; + list.push(listener); + platformListeners.set(id, list); +} + +function removePlatformListener(id: string, listener: PlatformChangeListener): void { + const list = platformListeners.get(id) ?? []; + platformListeners.set( + id, + list.filter((item) => item !== listener), + ); +} + +function addFrameworkListener(id: string, listener: FrameworkChangeListener): void { + const list = frameworkListeners.get(id) ?? []; + list.push(listener); + frameworkListeners.set(id, list); +} + +function removeFrameworkListener(id: string, listener: FrameworkChangeListener): void { + const list = frameworkListeners.get(id) ?? []; + frameworkListeners.set( + id, + list.filter((item) => item !== listener), + ); +} + +function broadcastPlatformChange(platform: string): void { + globalSelectedPlatform = platform; + // Store in sessionStorage for persistence across page loads + if (typeof window !== 'undefined') { + sessionStorage.setItem('stack-docs-selected-platform', platform); + } + + // Notify all listeners + for (const listeners of platformListeners.values()) { + listeners.forEach(listener => listener(platform)); + } +} + +function broadcastFrameworkChange(platform: string, framework: string): void { + globalSelectedFrameworks[platform] = framework; + // Store in sessionStorage for persistence across page loads + if (typeof window !== 'undefined') { + sessionStorage.setItem('stack-docs-selected-frameworks', JSON.stringify(globalSelectedFrameworks)); + } + + // Notify all listeners + for (const listeners of frameworkListeners.values()) { + listeners.forEach(listener => listener(platform, framework)); + } +} + +export interface PlatformCodeblockProps { + /** + * Platform configurations with their frameworks and code examples + */ + platforms: { + [platformName: string]: { + [frameworkName: string]: { + code: string; + language?: string; + filename?: string; + }; + }; + }; + /** + * Default platform to show + */ + defaultPlatform?: string; + /** + * Default framework to show for each platform + */ + defaultFrameworks?: { [platformName: string]: string }; + /** + * Optional title for the code block + */ + title?: string; + /** + * Additional CSS classes + */ + className?: string; +} + +export function PlatformCodeblock({ + platforms, + defaultPlatform, + defaultFrameworks = {}, + title, + className +}: PlatformCodeblockProps) { + const platformNames = Object.keys(platforms); + const firstPlatform = defaultPlatform || platformNames[0]; + + // Initialize with global platform or default + const getInitialPlatform = () => { + if (typeof window !== 'undefined') { + const stored = sessionStorage.getItem('stack-docs-selected-platform'); + if (stored && platformNames.includes(stored)) { + return stored; + } + } + return globalSelectedPlatform && platformNames.includes(globalSelectedPlatform) + ? globalSelectedPlatform + : firstPlatform; + }; + + // Initialize global frameworks with defaults if not already set + const initializeGlobalFrameworks = () => { + if (typeof window !== 'undefined') { + const stored = sessionStorage.getItem('stack-docs-selected-frameworks'); + if (stored) { + try { + const parsed = JSON.parse(stored); + globalSelectedFrameworks = { ...globalSelectedFrameworks, ...parsed }; + } catch (e) { + // Ignore parsing errors + } + } + } + + // Set defaults for any platforms that don't have a framework selected + platformNames.forEach(platform => { + if (!globalSelectedFrameworks[platform]) { + const frameworks = Object.keys(platforms[platform]); + globalSelectedFrameworks[platform] = defaultFrameworks?.[platform] || frameworks[0]; + } + }); + }; + + // Initialize global state on first render + useState(() => { + initializeGlobalFrameworks(); + }); + + const [selectedPlatform, setSelectedPlatform] = useState(getInitialPlatform); + const [selectedFrameworks, setSelectedFrameworks] = useState<{ [platform: string]: string }>(() => { + return { ...globalSelectedFrameworks }; + }); + + const [highlightedCode, setHighlightedCode] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [dropdownView, setDropdownView] = useState<'platform' | 'framework'>('platform'); + const [componentId] = useState(() => Math.random().toString(36).substr(2, 9)); + + // Get current framework options for selected platform + const currentFrameworks = Object.keys(platforms[selectedPlatform] || {}); + const currentFramework = selectedFrameworks[selectedPlatform] || currentFrameworks[0]; + const currentCodeConfig = platforms[selectedPlatform]?.[currentFramework]; + + // Set up global platform synchronization + useEffect(() => { + const onPlatformChange = (platform: string) => { + if (platformNames.includes(platform) && platform !== selectedPlatform) { + setSelectedPlatform(platform); + } + }; + + addPlatformListener(componentId, onPlatformChange); + + return () => { + removePlatformListener(componentId, onPlatformChange); + }; + }, [componentId, platformNames, selectedPlatform]); + + // Set up global framework synchronization + useEffect(() => { + const onFrameworkChange = (platform: string, framework: string) => { + // Only update if this platform exists in our platforms and the framework is available + if (platforms[platform] && Object.keys(platforms[platform]).includes(framework)) { + setSelectedFrameworks(prev => ({ + ...prev, + [platform]: framework + })); + } + }; + + addFrameworkListener(componentId, onFrameworkChange); + + return () => { + removeFrameworkListener(componentId, onFrameworkChange); + }; + }, [componentId, platforms]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest(`[data-dropdown-id="${componentId}"]`)) { + setIsDropdownOpen(false); + setDropdownView('platform'); + } + }; + + if (isDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [isDropdownOpen, componentId]); + + // Update highlighted code when selection changes + useEffect(() => { + if (!currentCodeConfig) return; + + const updateHighlightedCode = async () => { + try { + // Detect if we're in dark mode + const isDarkMode = document.documentElement.classList.contains('dark') || + getComputedStyle(document.documentElement).getPropertyValue('--fd-background').includes('0 0% 3.9%'); + + const theme = isDarkMode ? 'github-dark' : 'github-light'; + + const html = await codeToHtml(currentCodeConfig.code, { + lang: currentCodeConfig.language || 'typescript', + theme, + transformers: [{ + pre(node) { + // Remove background styles from pre element + if (node.properties.style) { + node.properties.style = (node.properties.style as string).replace(/background[^;]*;?/g, ''); + } + }, + code(node) { + // Remove background styles from code element + if (node.properties.style) { + node.properties.style = (node.properties.style as string).replace(/background[^;]*;?/g, ''); + } + // Add consistent styling + const existingStyle = (node.properties.style as string) || ''; + node.properties.style = `${existingStyle}; line-height: 1.5; font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; white-space: pre;`; + } + }] + }); + setHighlightedCode(html); + } catch (error) { + console.error('Error highlighting code:', error); + setHighlightedCode(`
    ${currentCodeConfig.code}
    `); + } + }; + + updateHighlightedCode(); + + // Listen for theme changes + const observer = new MutationObserver(() => { + updateHighlightedCode(); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + + return () => observer.disconnect(); + }, [currentCodeConfig]); + + const handlePlatformSelect = (platform: string) => { + broadcastPlatformChange(platform); + // Show framework selection for this platform + setDropdownView('framework'); + + // Auto-select first framework of new platform + const newPlatformFrameworks = Object.keys(platforms[platform] || {}); + if (newPlatformFrameworks.length > 0) { + const firstFramework = defaultFrameworks?.[platform] || newPlatformFrameworks[0]; + broadcastFrameworkChange(platform, firstFramework); + } + }; + + const handleFrameworkSelect = (framework: string) => { + broadcastFrameworkChange(selectedPlatform, framework); + setIsDropdownOpen(false); + setDropdownView('platform'); + }; + + const handleDropdownToggle = () => { + setIsDropdownOpen(!isDropdownOpen); + setDropdownView('platform'); + }; + + if (platformNames.length === 0) { + return
    No platforms configured
    ; + } + + return ( +
    +
    + {title && ( +
    +
    {title}
    +
    + )} + + {/* Single Cascading Dropdown */} +
    + {/* Current Selection Display */} +
    + {selectedPlatform} + / + {currentFramework} +
    + + {/* Cascading Dropdown - Right Side */} +
    + +
    +
    + + {/* File Title Bar */} + {currentCodeConfig?.filename && ( +
    +
    + {currentCodeConfig.filename} +
    +
    + )} + + {/* Code Content */} +
    +
    +
    +
    +
    +
    + + {/* Single Cascading Dropdown Menu */} + {isDropdownOpen && ( +
    + {dropdownView === 'platform' ? ( + // Platform Selection View +
    +
    + Select Platform +
    + {platformNames.map((platform) => ( + + ))} +
    + ) : ( + // Framework Selection View +
    +
    + + Select {selectedPlatform} Framework +
    + {currentFrameworks.map((framework) => ( + + ))} +
    + )} +
    + )} +
    + ); +} diff --git a/docs/src/components/mdx/platform-config.ts b/docs/src/components/mdx/platform-config.ts new file mode 100644 index 0000000000..275ac7f02a --- /dev/null +++ b/docs/src/components/mdx/platform-config.ts @@ -0,0 +1,159 @@ +// Centralized platform and framework configuration +export interface PlatformConfig { + [platformName: string]: { + [frameworkName: string]: { + defaultFilename?: string; + language: string; + }; + }; +} + +export const PLATFORM_CONFIG: PlatformConfig = { + "Python": { + "Django": { + defaultFilename: "views.py", + language: "python" + }, + "FastAPI": { + defaultFilename: "main.py", + language: "python" + }, + "Flask": { + defaultFilename: "app.py", + language: "python" + } + }, + "JavaScript": { + "Next.js": { + defaultFilename: "app/api/route.ts", + language: "typescript" + }, + "Express": { + defaultFilename: "server.js", + language: "javascript" + }, + "React": { + defaultFilename: "components/LoginForm.tsx", + language: "typescript" + }, + "Node.js": { + defaultFilename: "index.js", + language: "javascript" + } + }, + "TypeScript": { + "Next.js": { + defaultFilename: "app/api/route.ts", + language: "typescript" + }, + "Express": { + defaultFilename: "server.ts", + language: "typescript" + }, + "React": { + defaultFilename: "components/LoginForm.tsx", + language: "typescript" + }, + "Node.js": { + defaultFilename: "index.ts", + language: "typescript" + } + }, + "Go": { + "Gin": { + defaultFilename: "main.go", + language: "go" + }, + "Echo": { + defaultFilename: "server.go", + language: "go" + }, + "Standard Library": { + defaultFilename: "handler.go", + language: "go" + } + }, + "Java": { + "Spring Boot": { + defaultFilename: "AuthController.java", + language: "java" + }, + "Spring Security": { + defaultFilename: "SecurityConfig.java", + language: "java" + } + }, + "C#": { + ".NET Core": { + defaultFilename: "AuthController.cs", + language: "csharp" + }, + "ASP.NET": { + defaultFilename: "LoginController.cs", + language: "csharp" + } + }, + "PHP": { + "Laravel": { + defaultFilename: "AuthController.php", + language: "php" + }, + "Symfony": { + defaultFilename: "SecurityController.php", + language: "php" + }, + "Plain PHP": { + defaultFilename: "login.php", + language: "php" + } + }, + "Ruby": { + "Rails": { + defaultFilename: "sessions_controller.rb", + language: "ruby" + }, + "Sinatra": { + defaultFilename: "app.rb", + language: "ruby" + } + }, + "Rust": { + "Axum": { + defaultFilename: "main.rs", + language: "rust" + }, + "Actix": { + defaultFilename: "handlers.rs", + language: "rust" + } + } +}; + +// Helper function to get available platforms +export function getAvailablePlatforms(): string[] { + return Object.keys(PLATFORM_CONFIG); +} + +// Helper function to get frameworks for a platform +export function getFrameworksForPlatform(platform: string): string[] { + return Object.keys(PLATFORM_CONFIG[platform] || {}); +} + +// Helper function to get config for a platform/framework combination +export function getPlatformFrameworkConfig(platform: string, framework: string) { + return PLATFORM_CONFIG[platform]?.[framework]; +} + +// Default framework preferences (can be overridden) +export const DEFAULT_FRAMEWORK_PREFERENCES: { [platform: string]: string } = { + "Python": "Django", + "JavaScript": "Next.js", + "TypeScript": "Next.js", + "Go": "Gin", + "Java": "Spring Boot", + "C#": ".NET Core", + "PHP": "Laravel", + "Ruby": "Rails", + "Rust": "Axum" +}; + diff --git a/docs/src/components/mdx/simple-platform-codeblock.tsx b/docs/src/components/mdx/simple-platform-codeblock.tsx new file mode 100644 index 0000000000..3b0eb358b6 --- /dev/null +++ b/docs/src/components/mdx/simple-platform-codeblock.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { PlatformCodeblock, PlatformCodeblockProps } from './platform-codeblock'; +import { DEFAULT_FRAMEWORK_PREFERENCES, getPlatformFrameworkConfig } from './platform-config'; + +// Simplified interface - just provide the code for each platform/framework +export interface SimplePlatformCodeblockProps { + /** + * Code examples organized by platform and framework + * Uses the centralized platform config for language and filename defaults + */ + code: { + [platformName: string]: { + [frameworkName: string]: string | { + code: string; + filename?: string; // Override default filename + }; + }; + }; + /** + * Which platforms to include (defaults to all platforms in the code object) + */ + platforms?: string[]; + /** + * Default platform to show + */ + defaultPlatform?: string; + /** + * Override default framework preferences + */ + defaultFrameworks?: { [platformName: string]: string }; + /** + * Optional title for the code block + */ + title?: string; + /** + * Additional CSS classes + */ + className?: string; +} + +export function SimplePlatformCodeblock({ + code, + platforms: includedPlatforms, + defaultPlatform, + defaultFrameworks, + title, + className +}: SimplePlatformCodeblockProps) { + // Determine which platforms to include + const availablePlatforms = Object.keys(code); + const platformsToShow = includedPlatforms || availablePlatforms; + + // Build the full platform configuration + const fullPlatforms: PlatformCodeblockProps['platforms'] = {}; + + platformsToShow.forEach(platform => { + if (!code[platform]) return; + + fullPlatforms[platform] = {}; + + Object.entries(code[platform]).forEach(([framework, codeData]) => { + const config = getPlatformFrameworkConfig(platform, framework); + if (!config) { + console.warn(`Unknown platform/framework combination: ${platform}/${framework}`); + return; + } + + const codeString = typeof codeData === 'string' ? codeData : codeData.code; + const filename = typeof codeData === 'object' && codeData.filename + ? codeData.filename + : config.defaultFilename; + + fullPlatforms[platform][framework] = { + code: codeString, + language: config.language, + filename + }; + }); + }); + + // Merge default framework preferences + const mergedDefaultFrameworks = { + ...DEFAULT_FRAMEWORK_PREFERENCES, + ...defaultFrameworks + }; + + return ( + + ); +} + +// Export the centralized config for direct access if needed +export { DEFAULT_FRAMEWORK_PREFERENCES, PLATFORM_CONFIG } from './platform-config'; + + diff --git a/docs/src/components/mdx/simple-platform-example.mdx b/docs/src/components/mdx/simple-platform-example.mdx new file mode 100644 index 0000000000..dfba30fbb7 --- /dev/null +++ b/docs/src/components/mdx/simple-platform-example.mdx @@ -0,0 +1,120 @@ +# Simplified Platform Codeblock Example + +## Before (Verbose) + +```tsx + +``` + +## After (Simplified) + + { + const { username, password } = req.body; + + // Authentication logic here + res.json({ success: true }); +});` + } + }} +/> + +## Benefits + +1. **No repetition**: Platform names, framework names, languages, and default filenames are centralized +2. **Consistency**: All instances use the same naming and configuration +3. **Maintainability**: Add new platforms/frameworks in one place +4. **Simplicity**: Just provide the code, everything else is automatic +5. **Flexibility**: Can still override filenames when needed + +## Custom Filename Example + + + +## Platform Filtering + + + diff --git a/docs/src/mdx-components.tsx b/docs/src/mdx-components.tsx index 43ab06d604..dc08848d92 100644 --- a/docs/src/mdx-components.tsx +++ b/docs/src/mdx-components.tsx @@ -13,6 +13,8 @@ import { Card, CardGroup, Info } from './components/mdx'; import ApiSequenceDiagram from './components/mdx/api-sequence-diagram'; import { AuthCard } from './components/mdx/auth-card'; import { DynamicCodeblock } from './components/mdx/dynamic-code-block'; +import { PlatformCodeblock } from './components/mdx/platform-codeblock'; +import { SimplePlatformCodeblock } from './components/mdx/simple-platform-codeblock'; import { AsideSection, CollapsibleMethodSection, CollapsibleTypesSection, MethodAside, MethodContent, MethodLayout, MethodSection, MethodTitle } from './components/ui/method-layout'; @@ -68,6 +70,8 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { CodeBlocks, Icon, DynamicCodeblock, + PlatformCodeblock, + SimplePlatformCodeblock, Mermaid, MethodLayout, MethodContent, From 70e686880d62f2904b70c2529945495531bda2ed Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 1 Oct 2025 09:53:41 -0500 Subject: [PATCH 05/55] Remove codeblock sync demo file --- .../mdx/platform-codeblock-sync-demo.tsx | 103 ------------------ 1 file changed, 103 deletions(-) delete mode 100644 docs/src/components/mdx/platform-codeblock-sync-demo.tsx diff --git a/docs/src/components/mdx/platform-codeblock-sync-demo.tsx b/docs/src/components/mdx/platform-codeblock-sync-demo.tsx deleted file mode 100644 index d50fa3deee..0000000000 --- a/docs/src/components/mdx/platform-codeblock-sync-demo.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { PlatformCodeblock } from './platform-codeblock'; - -// Demo component to test multi-instance synchronization -export function PlatformCodeblockSyncDemo() { - const samplePlatforms = { - "Python": { - "Django": { - code: `# Django Authentication -from django.contrib.auth import authenticate, login -from django.http import JsonResponse - -def login_view(request): - user = authenticate(username='user', password='pass') - if user: - login(request, user) - return JsonResponse({'success': True})`, - language: "python", - filename: "views.py" - }, - "FastAPI": { - code: `# FastAPI Authentication -from fastapi import FastAPI, HTTPException - -app = FastAPI() - -@app.post("/login") -async def login(): - # Authentication logic here - return {"access_token": "token", "token_type": "bearer"}`, - language: "python", - filename: "main.py" - } - }, - "JavaScript": { - "Next.js": { - code: `// Next.js API Route -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - const { username, password } = await request.json(); - - // Authentication logic here - return NextResponse.json({ success: true }); -}`, - language: "typescript", - filename: "app/api/login/route.ts" - }, - "Express": { - code: `// Express.js Route -const express = require('express'); -const app = express(); - -app.post('/login', async (req, res) => { - const { username, password } = req.body; - - // Authentication logic here - res.json({ success: true }); -});`, - language: "javascript", - filename: "server.js" - } - } - }; - - return ( -
    -
    -

    First Instance

    - -
    - -
    -

    Second Instance

    - -
    - -
    -

    - Test Single Cascading Dropdown: Click "Change" to open the dropdown, select a platform, then select a framework within that platform! -

    -

    - Navigation: Use the "Back" button to return to platform selection from framework selection! -

    -

    - Cross-Instance Sync: Change selections in one instance and watch both instances update simultaneously! -

    -

    - Persistence: Refresh the page and your selections will be remembered across sessions! -

    -
    -
    - ); -} From eb2293c7d7ceda8e03f8dead9507efdbd31438fc Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 1 Oct 2025 10:15:09 -0500 Subject: [PATCH 06/55] fix some lint --- .../platform-codeblock-example.mdx | 1 - .../components/layouts/docs-layout-router.tsx | 3 +- .../src/components/mdx/platform-codeblock.tsx | 184 ++++++++++++++---- docs/src/components/mdx/platform-config.ts | 20 +- .../mdx/simple-platform-codeblock.tsx | 39 ++-- 5 files changed, 174 insertions(+), 73 deletions(-) diff --git a/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx b/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx index c9cb0e1599..49228d2017 100644 --- a/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx +++ b/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx @@ -300,7 +300,6 @@ export function LoginForm({ onLoginSuccess }: LoginFormProps) { }} /> - listener(platform)); @@ -62,54 +62,70 @@ function broadcastFrameworkChange(platform: string, framework: string): void { if (typeof window !== 'undefined') { sessionStorage.setItem('stack-docs-selected-frameworks', JSON.stringify(globalSelectedFrameworks)); } - // Notify all listeners for (const listeners of frameworkListeners.values()) { listeners.forEach(listener => listener(platform, framework)); } } -export interface PlatformCodeblockProps { +export type PlatformCodeblockProps = { /** * Platform configurations with their frameworks and code examples + * Can include server/client variants for each framework */ platforms: { [platformName: string]: { [frameworkName: string]: { - code: string; - language?: string; - filename?: string; - }; - }; - }; + code: string, + language?: string, + filename?: string, + } | { + server: { + code: string, + language?: string, + filename?: string, + }, + client: { + code: string, + language?: string, + filename?: string, + }, + }, + }, + }, /** * Default platform to show */ - defaultPlatform?: string; + defaultPlatform?: string, /** * Default framework to show for each platform */ - defaultFrameworks?: { [platformName: string]: string }; + defaultFrameworks?: { [platformName: string]: string }, + /** + * Default server/client selection for each platform/framework + */ + defaultVariants?: { [platformName: string]: { [frameworkName: string]: 'server' | 'client' } }, /** * Optional title for the code block */ - title?: string; + title?: string, /** * Additional CSS classes */ - className?: string; + className?: string, } export function PlatformCodeblock({ platforms, defaultPlatform, defaultFrameworks = {}, + defaultVariants = {}, title, className }: PlatformCodeblockProps) { const platformNames = Object.keys(platforms); const firstPlatform = defaultPlatform || platformNames[0]; - + // Initialize with global platform or default const getInitialPlatform = () => { if (typeof window !== 'undefined') { @@ -118,8 +134,8 @@ export function PlatformCodeblock({ return stored; } } - return globalSelectedPlatform && platformNames.includes(globalSelectedPlatform) - ? globalSelectedPlatform + return globalSelectedPlatform && platformNames.includes(globalSelectedPlatform) + ? globalSelectedPlatform : firstPlatform; }; @@ -141,7 +157,7 @@ export function PlatformCodeblock({ platformNames.forEach(platform => { if (!globalSelectedFrameworks[platform]) { const frameworks = Object.keys(platforms[platform]); - globalSelectedFrameworks[platform] = defaultFrameworks?.[platform] || frameworks[0]; + globalSelectedFrameworks[platform] = defaultFrameworks[platform] || frameworks[0]; } }); }; @@ -150,21 +166,66 @@ export function PlatformCodeblock({ useState(() => { initializeGlobalFrameworks(); }); - + const [selectedPlatform, setSelectedPlatform] = useState(getInitialPlatform); const [selectedFrameworks, setSelectedFrameworks] = useState<{ [platform: string]: string }>(() => { return { ...globalSelectedFrameworks }; }); - + const [selectedVariants, setSelectedVariants] = useState<{ [platform: string]: { [framework: string]: 'server' | 'client' } }>(() => { + return { ...defaultVariants }; + }); + const [highlightedCode, setHighlightedCode] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [dropdownView, setDropdownView] = useState<'platform' | 'framework'>('platform'); const [componentId] = useState(() => Math.random().toString(36).substr(2, 9)); // Get current framework options for selected platform - const currentFrameworks = Object.keys(platforms[selectedPlatform] || {}); + const currentFrameworks = Object.keys(platforms[selectedPlatform] ?? {}); const currentFramework = selectedFrameworks[selectedPlatform] || currentFrameworks[0]; - const currentCodeConfig = platforms[selectedPlatform]?.[currentFramework]; + + // Helper functions for server/client variants + const hasVariants = (platform: string, framework: string) => { + const platformConfig = platforms[platform]; + if (!platformConfig) { + return false; + } + + const config = platformConfig[framework]; + if (!config || typeof config !== 'object') { + return false; + } + + return 'server' in config && 'client' in config; + }; + + const getCurrentVariant = (): 'server' | 'client' => { + const platformVariants = selectedVariants[selectedPlatform]; + if (platformVariants && platformVariants[currentFramework]) { + return platformVariants[currentFramework]; + } + + return 'server'; + }; + + const getCurrentCodeConfig = () => { + const platformConfig = platforms[selectedPlatform]; + if (!platformConfig) { + return null; + } + + const config = platformConfig[currentFramework]; + if (!config) return null; + + if (hasVariants(selectedPlatform, currentFramework)) { + const variant = getCurrentVariant(); + return (config as { server: { code: string, language?: string, filename?: string }, client: { code: string, language?: string, filename?: string } })[variant]; + } + + return config as { code: string, language?: string, filename?: string }; + }; + + const currentCodeConfig = getCurrentCodeConfig(); // Set up global platform synchronization useEffect(() => { @@ -185,7 +246,7 @@ export function PlatformCodeblock({ useEffect(() => { const onFrameworkChange = (platform: string, framework: string) => { // Only update if this platform exists in our platforms and the framework is available - if (platforms[platform] && Object.keys(platforms[platform]).includes(framework)) { + if (platform in platforms && Object.keys(platforms[platform]).includes(framework)) { setSelectedFrameworks(prev => ({ ...prev, [platform]: framework @@ -256,11 +317,11 @@ export function PlatformCodeblock({ } }; - updateHighlightedCode(); + runAsynchronously(updateHighlightedCode); // Listen for theme changes const observer = new MutationObserver(() => { - updateHighlightedCode(); + runAsynchronously(updateHighlightedCode); }); observer.observe(document.documentElement, { @@ -275,11 +336,11 @@ export function PlatformCodeblock({ broadcastPlatformChange(platform); // Show framework selection for this platform setDropdownView('framework'); - + // Auto-select first framework of new platform - const newPlatformFrameworks = Object.keys(platforms[platform] || {}); + const newPlatformFrameworks = Object.keys(platforms[platform] ?? {}); if (newPlatformFrameworks.length > 0) { - const firstFramework = defaultFrameworks?.[platform] || newPlatformFrameworks[0]; + const firstFramework = defaultFrameworks[platform] || newPlatformFrameworks[0]; broadcastFrameworkChange(platform, firstFramework); } }; @@ -295,6 +356,16 @@ export function PlatformCodeblock({ setDropdownView('platform'); }; + const handleVariantChange = (variant: 'server' | 'client') => { + setSelectedVariants(prev => ({ + ...prev, + [selectedPlatform]: { + ...prev[selectedPlatform], + [currentFramework]: variant + } + })); + }; + if (platformNames.length === 0) { return
    No platforms configured
    ; } @@ -307,17 +378,41 @@ export function PlatformCodeblock({
    {title}
    )} - - {/* Single Cascading Dropdown */} + + {/* Server/Client Tabs and Cascading Dropdown */}
    - {/* Current Selection Display */} -
    - {selectedPlatform} - / - {currentFramework} + {/* Left Side: Server/Client Tabs or Current Selection */} +
    + {hasVariants(selectedPlatform, currentFramework) ? ( + // Show Server/Client tabs when variants exist +
    + {(['server', 'client'] as const).map((variant) => ( + + ))} +
    + ) : ( + // Show current selection when no variants +
    + {selectedPlatform} + / + {currentFramework} +
    + )}
    - {/* Cascading Dropdown - Right Side */} + {/* Right Side: Cascading Dropdown */}
    - @@ -969,24 +969,14 @@ export function DocsLayoutSidebar({ {collapsible ? : null} {/* Sidebar positioned under the header */}
    -
    +
    {/* Scrollable content area */} -
    +
    - {!isMainSidebarCollapsed && ( - - - Back to home - - )} - {/* Platform tabs/banner */} {banner && !isMainSidebarCollapsed && (
    @@ -1091,3 +1081,4 @@ function CodeOverlayRenderer() { export { getSidebarTabsFromOptions } from './docs/shared'; export { CollapsibleControl, Navbar, NavbarSidebarTrigger, type LinkItemType }; + diff --git a/docs/src/components/layouts/page.tsx b/docs/src/components/layouts/page.tsx index 3d218452a0..7e3e972b1d 100644 --- a/docs/src/components/layouts/page.tsx +++ b/docs/src/components/layouts/page.tsx @@ -4,22 +4,22 @@ import type { TableOfContents } from 'fumadocs-core/server'; import { AnchorProvider, type AnchorProviderProps } from 'fumadocs-core/toc'; import { I18nLabel } from 'fumadocs-ui/contexts/i18n'; import { Edit, Text } from 'lucide-react'; -import { type ComponentProps, forwardRef, lazy, type ReactNode, useEffect } from 'react'; +import { forwardRef, lazy, useEffect, type ComponentProps, type ReactNode } from 'react'; import { cn } from '../../lib/cn'; import { - Toc, TOCItems, - type TOCProps, TOCScrollArea, + Toc, + type TOCProps, } from '../layout/toc'; import { Breadcrumb, - type BreadcrumbProps, Footer, - type FooterProps, LastUpdate, PageArticle, - PageBody + PageBody, + type BreadcrumbProps, + type FooterProps } from '../page-client'; import { BackToTop } from '../ui/back-to-top'; import { buttonVariants } from '../ui/button'; @@ -156,7 +156,7 @@ export function DocsPage({ className={cn('relative', props.article?.className)} > {slot(props.breadcrumb, )} -
    +
    {props.children}
    diff --git a/docs/src/components/layouts/shared-header.tsx b/docs/src/components/layouts/shared-header.tsx index 082a3b56d9..387c1e82aa 100644 --- a/docs/src/components/layouts/shared-header.tsx +++ b/docs/src/components/layouts/shared-header.tsx @@ -1,7 +1,6 @@ 'use client'; import { CustomSearchDialog } from '@/components/layout/custom-search-dialog'; import { SearchInputToggle } from '@/components/layout/custom-search-toggle'; -import Waves from '@/components/layouts/api/waves'; import { type NavLink } from '@/lib/navigation-utils'; import { UserButton } from '@stackframe/stack'; import { Key, Menu, Sparkles, TableOfContents, X } from 'lucide-react'; @@ -17,8 +16,6 @@ type SharedHeaderProps = { navLinks: NavLink[], /** Whether to show the search bar */ showSearch?: boolean, - /** Custom positioning classes - defaults to fixed positioning for docs */ - className?: string, /** Additional content to render after nav links */ children?: ReactNode, /** Mobile menu click handler */ @@ -63,7 +60,7 @@ function isNavLinkActive(pathname: string, navLink: NavLink): boolean { if (navLink.label === 'API Reference' && isInApiSection(pathname)) { return true; } - if (navLink.label === 'Documentation' && pathname.startsWith('/docs') && + if (navLink.label === 'Guides' && pathname.startsWith('/docs') && !isInComponentsSection(pathname) && !isInSdkSection(pathname)) { return true; } @@ -73,7 +70,7 @@ function isNavLinkActive(pathname: string, navLink: NavLink): boolean { /** * AI Chat Toggle Button */ -function AIChatToggleButton() { +function AIChatToggleButton(props: { className: string }) { const sidebarContext = useSidebar(); // Return null if context is not available @@ -87,7 +84,8 @@ function AIChatToggleButton() { +
    +
    +
    + {/* Second row */} +
    {/* Desktop Navigation Links - Hidden on mobile */} -
    +
    {navLinks.map((link, index) => { const isActive = isNavLinkActive(pathname, link); const IconComponent = link.icon; @@ -310,56 +345,6 @@ export function SharedHeader({ {children}
    - - {/* Right side - Mobile Menu and Search */} -
    - {/* Search Bar - Responsive sizing */} - {showSearch && ( - <> -
    - setSearchOpen(true)} - /> -
    - - - )} - - {/* TOC Toggle Button - Only on docs pages */} -
    - -
    - - {/* Auth Toggle Button - Shows on all pages like AI Chat button */} -
    - -
    - - {/* AI Chat Toggle Button */} -
    - -
    - - {/* User Button */} -
    - -
    - - {/* Mobile Hamburger Menu - Shown on mobile */} -
    - -
    -
    {/* Mobile Navigation Overlay */} diff --git a/docs/src/components/layouts/sidebar-context.tsx b/docs/src/components/layouts/sidebar-context.tsx index b503ba2d50..338f77c5dc 100644 --- a/docs/src/components/layouts/sidebar-context.tsx +++ b/docs/src/components/layouts/sidebar-context.tsx @@ -112,7 +112,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) { } else { setActiveSidebar(null); localStorage.setItem('ai-chat-open', 'false'); - setIsChatExpanded(false); + setChatExpanded(false); } }; @@ -153,7 +153,7 @@ export function SidebarProvider({ children }: { children: ReactNode }) { const closeSidebar = () => { setActiveSidebar(null); localStorage.setItem('ai-chat-open', 'false'); - setIsChatExpanded(false); + setChatExpanded(false); }; return ( diff --git a/docs/src/components/mdx/dynamic-code-block-overlay.tsx b/docs/src/components/mdx/dynamic-code-block-overlay.tsx index d44070b3fc..27dcf95a5a 100644 --- a/docs/src/components/mdx/dynamic-code-block-overlay.tsx +++ b/docs/src/components/mdx/dynamic-code-block-overlay.tsx @@ -161,9 +161,9 @@ export function DynamicCodeblockOverlay({ {/* Overlay - positioned to not overlap sidebar */}
    {/* Header */} -
    +
    From 19ccd7714da1bf466ef5a05ed23dc1f454b55ee2 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 1 Oct 2025 14:28:03 -0500 Subject: [PATCH 11/55] Weaker code block shadow --- docs/src/components/mdx/dynamic-code-block-overlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/components/mdx/dynamic-code-block-overlay.tsx b/docs/src/components/mdx/dynamic-code-block-overlay.tsx index 27dcf95a5a..72840e52c1 100644 --- a/docs/src/components/mdx/dynamic-code-block-overlay.tsx +++ b/docs/src/components/mdx/dynamic-code-block-overlay.tsx @@ -163,7 +163,7 @@ export function DynamicCodeblockOverlay({ className={cn( "sticky bottom-0 mx-2 bg-fd-background border-t border-r border-l rounded-tl-xl rounded-tr-xl border-fd-border z-50", "transition-all duration-300 ease-out", - "shadow-2xl shadow-black/30 dark:shadow-white/30", + "shadow-2xl shadow-black/20 dark:shadow-white/20", "flex flex-col", // Add flex container // Position to avoid sidebar overlap - adjust based on sidebar state "left-0 right-0", From ba1b1c98de6f2d849c4f4d67314ea0b2e6888bcf Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 1 Oct 2025 11:36:24 -0500 Subject: [PATCH 12/55] useEffect instead --- docs/src/components/mdx/platform-codeblock.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/src/components/mdx/platform-codeblock.tsx b/docs/src/components/mdx/platform-codeblock.tsx index bb246ab81b..632cf8e453 100644 --- a/docs/src/components/mdx/platform-codeblock.tsx +++ b/docs/src/components/mdx/platform-codeblock.tsx @@ -165,7 +165,7 @@ export function PlatformCodeblock({ }; // Initialize global state on first render - useState(() => { + useEffect(() => { initializeGlobalFrameworks(); }); @@ -286,7 +286,11 @@ export function PlatformCodeblock({ const theme = isDarkMode ? 'github-dark' : 'github-light'; - const html = await codeToHtml(currentCodeConfig.code, { + const codeToHighlight = currentCodeConfig.code.startsWith(' ') + ? currentCodeConfig.code.slice(1) + : currentCodeConfig.code; + + const html = await codeToHtml(codeToHighlight, { lang: currentCodeConfig.language || 'typescript', theme, transformers: [{ @@ -310,7 +314,10 @@ export function PlatformCodeblock({ setHighlightedCode(html); } catch (error) { console.error('Error highlighting code:', error); - setHighlightedCode(`
    ${currentCodeConfig.code}
    `); + const sanitized = currentCodeConfig.code.startsWith(' ') + ? currentCodeConfig.code.slice(1) + : currentCodeConfig.code; + setHighlightedCode(`
    ${sanitized}
    `); } }; From f364a0b7cef274fa1a3b27e1e4f779efc7eebd69 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 1 Oct 2025 12:32:19 -0500 Subject: [PATCH 13/55] init setup docs new --- .../docs/(guides)/getting-started/setup.mdx | 1146 ++++++++++------- 1 file changed, 685 insertions(+), 461 deletions(-) diff --git a/docs/content/docs/(guides)/getting-started/setup.mdx b/docs/content/docs/(guides)/getting-started/setup.mdx index 2c6c838450..153a34b67e 100644 --- a/docs/content/docs/(guides)/getting-started/setup.mdx +++ b/docs/content/docs/(guides)/getting-started/setup.mdx @@ -2,21 +2,28 @@ title: Setup --- -{/* IF_PLATFORM: next */} - -Welcome to the Next.js SDK setup guide. If you're looking for guides for other frameworks, check out the [React SDK Setup](/docs/react/getting-started/setup), or the [JavaScript SDK Setup](/docs/js/getting-started/setup). +The Stack Auth Docs contain code examples for various languages and frameworks. You can select the language and framework in any codeblock. ## Setup -Before getting started, make sure you have a [Next.js project](https://nextjs.org/docs/getting-started/installation) using the app router, as Stack Auth does not support the pages router. +Stack Auth supports multiple platforms and frameworks. Choose your preferred language and framework from the dropdown in the code examples below. + +### Prerequisites + +Before getting started, make sure you have a project set up for your chosen platform: + +- **Next.js**: A [Next.js project](https://nextjs.org/docs/getting-started/installation) using the app router (Stack Auth does not support the pages router on Next.js) +- **React**: A [React project](https://react.dev/learn/creating-a-react-app) (we show examples with Vite) +- **JavaScript**: A Node.js project with Express +- **Python**: A Python environment with your chosen framework (Django, FastAPI, or Flask) -We recommend using our **setup wizard** for a seamless installation experience. The wizard automatically detects your project structure and walks you through the setup process. If you encounter any issues with the wizard, you can follow our manual installation steps instead. +We recommend using our **setup wizard** for JavaScript frameworks for a seamless installation experience. For Python, we recommend using the REST API approach. - Setup wizard (recommended) + Setup wizard (recommended for JS) Manual installation @@ -25,6 +32,11 @@ We recommend using our **setup wizard** for a seamless installation experience. ### Run installation wizard + + + The setup wizard is available for JavaScript/TypeScript frameworks. For Python projects, please use the manual installation method. + + Run Stack's installation wizard with the following command: ```sh title="Terminal" @@ -34,190 +46,392 @@ We recommend using our **setup wizard** for a seamless installation experience. ### Update API keys - Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its environment variables into the `.env.local` file of your Next.js project: - - ```sh title=".env.local" - NEXT_PUBLIC_STACK_PROJECT_ID= - NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= - STACK_SECRET_SERVER_KEY= - ``` + + Create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its environment variables into the appropriate configuration file: + + +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + language: "bash", + filename: ".env.local" + }, + "React": { + code: `// Update the values in stack/client.ts created by the wizard +export const stackClientApp = new StackClientApp({ + projectId: "your-project-id", + publishableClientKey: "your-publishable-client-key", + tokenStore: "cookie", +});`, + language: "typescript", + filename: "stack/client.ts" + }, + "JavaScript": { + code: `STACK_PROJECT_ID= +STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + language: "bash", + filename: ".env" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js" }} + title="Environment Configuration" + /> ### Done! - That's it! The following files should have been created or updated in your project: + That's it! The wizard should have created or updated the following files in your project: + + **For Next.js:** + - `app/handler/[...stack]/page.tsx`: Default pages for sign-in, sign-out, account settings, and more + - `app/layout.tsx`: Updated to wrap the entire body with `StackProvider` and `StackTheme` + - `app/loading.tsx`: Suspense boundary for Stack's async hooks + - `stack/server.ts`: Contains the `stackServerApp` for server-side usage + - `stack/client.ts`: Contains the `stackClientApp` for client-side usage - - `app/handler/[...stack]/page.tsx`: This file contains the default pages for sign-in, sign-out, account settings, and more. If you prefer, later you will learn how to [use custom pages](../customization/custom-pages.mdx) instead. - - `app/layout.tsx`: The layout file was updated to wrap the entire body with `StackProvider` and `StackTheme`. - - `app/loading.tsx`: If not yet found, Stack automatically adds a Suspense boundary to your app. This is shown to the user while Stack's async hooks, like `useUser`, are loading. - - `stack/server.ts`: This file contains the `stackServerApp` which you can use to access Stack from Server Components, Server Actions, API routes, and middleware. - - `stack/client.ts`: This file contains the `stackClientApp` which you can use to access Stack from Client Components + **For React:** + - `stack/client.ts`: Contains the `stackClientApp` configuration + - Your app should be wrapped with `StackProvider` and `StackTheme` + + **For Node.js/Express:** + - `stack/server.ts`: Contains the `stackServerApp` configuration - Note: The setup wizard also supports existing, complicated projects. Cases where manual installation is necessary are rare. + Note: The setup wizard also supports existing, complicated projects. Cases where manual installation is necessary are rare for JavaScript frameworks. - If you are struggling with the setup wizard, please reach out to us on our [Discord](https://discord.stack-auth.com) first, where we'll be happy to help you. - ### Install npm package + ### Install package - First, install Stack with npm, yarn, or pnpm: - - ```bash title="Terminal" - npm install @stackframe/stack - ``` + + First, install the appropriate Stack package: + + ### Create API keys - If you haven't already, [register a new account on Stack](https://app.stack-auth.com/handler/sign-up). Create a project in the dashboard, create a new API key from the left sidebar, and copy the project ID, publishable client key, and secret server key into a new file called `.env.local` in the root of your Next.js project: + + [Register a new account on Stack](https://app.stack-auth.com/handler/sign-up), create a project in the dashboard, create a new API key from the left sidebar, and copy the project ID, publishable client key, and secret server key. - ```sh title=".env.local" - NEXT_PUBLIC_STACK_PROJECT_ID= - NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= - STACK_SECRET_SERVER_KEY= - ``` + + ### Configure environment variables + + + Set up your environment variables or configuration: + + +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + language: "env", + filename: ".env.local" + }, + "React": { + code: `# Store these in environment variables or directly in the client file during development +VITE_STACK_PROJECT_ID= +VITE_STACK_PUBLISHABLE_CLIENT_KEY=`, + language: "env", + filename: ".env" + }, + "Express": { + code: `STACK_PROJECT_ID= +STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + language: "env", + filename: ".env" + }, + "Node.js": { + code: `STACK_PROJECT_ID= +STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + language: "env", + filename: ".env" + } + }, + "Python": { + "Django": { + code: `import os + +stack_project_id = os.getenv("STACK_PROJECT_ID") +stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") +stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, + language: "python", + filename: "settings.py" + }, + "FastAPI": { + code: `import os + +stack_project_id = os.getenv("STACK_PROJECT_ID") +stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") +stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, + language: "python", + filename: "main.py" + }, + "Flask": { + code: `import os + +stack_project_id = os.getenv("STACK_PROJECT_ID") +stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") +stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, + language: "python", + filename: "app.py" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js", "Python": "Django" }} + title="Environment Configuration" + /> - ### Create `stack/server.ts` file + ### Create Stack configuration - Create a new file `stack/server.ts` in your root directory and fill it with the following: - ```tsx title="stack/server.ts" - import "server-only"; + Create the Stack app configuration: + + = 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json()`, + language: "python", + filename: "views.py" + }, + "FastAPI": { + code: `import requests + +def stack_auth_request(method, endpoint, **kwargs): + res = requests.request( + method, + f'https://api.stack-auth.com/{endpoint}', + headers={ + 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API + 'x-stack-project-id': stack_project_id, + 'x-stack-publishable-client-key': stack_publishable_client_key, + 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' + **kwargs.pop('headers', {}), + }, + **kwargs, + ) + if res.status_code >= 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json()`, + language: "python", + filename: "main.py" + }, + "Flask": { + code: `import requests + +def stack_auth_request(method, endpoint, **kwargs): + res = requests.request( + method, + f'https://api.stack-auth.com/{endpoint}', + headers={ + 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API + 'x-stack-project-id': stack_project_id, + 'x-stack-publishable-client-key': stack_publishable_client_key, + 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' + **kwargs.pop('headers', {}), + }, + **kwargs, + ) + if res.status_code >= 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json()`, + language: "python", + filename: "app.py" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js", "Python": "Django" }} + title="Stack Configuration" + /> - ### Create Stack handler + ### Set up authentication handlers (JavaScript frameworks only) - Create a new file in `app/handler/[...stack]/page.tsx` and paste the following code: + + For JavaScript frameworks, create the authentication handler: - ```tsx title="app/handler/[...stack]/page.tsx" - import { StackHandler } from "@stackframe/stack"; + ; - } - ``` - - This will create pages for sign-in, sign-up, password reset, and others. Additionally, it will be used as a callback URL for OAuth. You can [replace them with your own pages](../customization/custom-pages.mdx) later. - - - ### Add StackProvider to `layout.tsx` - - In your `app/layout.tsx`, wrap the entire body with a `StackProvider` and `StackTheme`. Afterwards, it should look like this: - - ```tsx title="app/layout.tsx" - import React from "react"; - import { StackProvider, StackTheme } from "@stackframe/stack"; - import { stackServerApp } from "@/stack/server"; - - export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - - - {children} - - - - - ); - } - ``` - - - ### Add Suspense boundary - - By default, Stack uses [`Suspense`](https://react.dev/reference/react/Suspense) to handle loading states. To show a loading indicator while Stack is fetching user data, make sure there is a `loading.tsx` file in your `app` directory: - - ```tsx title="app/loading.tsx" - export default function Loading() { - // You can use any loading indicator here - return <> - Loading... - ; - } - ``` - - - ### Done! - - - - - -## Post-setup - -That's it! Stack is now configured in your Next.js project. If you start your Next.js app with `npm run dev` and navigate to [http://localhost:3000/handler/signup](http://localhost:3000/handler/sign-up), you will see the sign-up page. - -
    - SignIn -
    - -After signing up/in, you will be redirected back to the home page. We will show you how to add user information to it in the next section. You can also check out the [http://localhost:3000/handler/account-settings](http://localhost:3000/handler/account-settings) page which looks like this: - -![Stack account settings page](/imgs/account-settings.png) - - -## Next steps - -Next up, we will show you how to [retrieve and update user information](./users.mdx), and how to [protect a page](./users.mdx#protecting-a-page) from unauthorized access. - -{/* ELSE_IF_PLATFORM react */} - - -Welcome to the React SDK setup guide! If you're looking for guides for other frameworks, check out the [Next.js SDK Setup](/docs/next/getting-started/setup), or the [JavaScript SDK Setup](/docs/js/getting-started/setup). - - -Before getting started, make sure you have a [React project](https://react.dev/learn/creating-a-react-app) setup. We show an example here of a Vite React project. - -We recommend using our **setup wizard** for a seamless installation experience. The wizard automatically detects your project structure and walks you through the setup process. If you encounter any issues with the wizard, you can follow our manual installation steps instead. - - - - Setup wizard (recommended) - Manual installation - - - - - - ### Run installation wizard - - Run Stack's installation wizard with the following command: - - ```sh title="Terminal" - npx @stackframe/init-stack@latest - ``` - - - ### Update API keys - - Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its values into the `stack/client.ts` file created by the wizard. - - - ### Wrap your app - - This example uses react-router, but all React apps should wrap the app with `StackProvider` and `StackTheme`. - - ```tsx title="App.tsx" - import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; +}`, + language: "typescript", + filename: "app/handler/[...stack]/page.tsx" + }, + "React": { + code: `import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; import { Suspense } from "react"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; import { stackClientApp } from "./stack/client"; @@ -244,352 +458,362 @@ We recommend using our **setup wizard** for a seamless installation experience. ); - } - ``` +}`, + language: "typescript", + filename: "App.tsx" + }, + "Express": { + code: `// Express doesn't use built-in handlers +// Use the REST API or integrate with your frontend`, + language: "typescript", + filename: "Note" + }, + "Node.js": { + code: `// Node.js doesn't use built-in handlers +// Use the REST API or integrate with your frontend`, + language: "javascript", + filename: "Note" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js" }} + title="Authentication Handlers" + /> - ### Done! + ### Add providers (Next.js and React only) - That's it! Stack is now configured in your React project. If you start your app and navigate to [http://localhost:5173/handler/sign-up](http://localhost:5173/handler/sign-up), you will see the sign-up page. -
    - SignIn -
    - - After signing up/in, you will be redirected back to the home page. You can also check out the [http://localhost:5173/handler/account-settings](http://localhost:5173/handler/account-settings) page. -
    -
    - - - - - ### Install npm package - - - ```bash title="Terminal" - npm install @stackframe/react - ``` + For Next.js and React, wrap your app with Stack providers: + + + + + + {children} + + + + + ); +}`, + language: "typescript", + filename: "app/layout.tsx" + }, + "React": { + code: `// Already shown in the App.tsx example above +// Make sure to wrap your app with StackProvider and StackTheme`, + language: "typescript", + filename: "Note" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js" }} + title="App Providers" + /> - ### Create API keys + ### Add loading boundary (Next.js only) - If you haven't already, [register a new account on Stack](https://app.stack-auth.com/projects), create a project in the dashboard, create a new API key from the left sidebar, and copy the project ID and publishable client key. Store them based on your project setup (environment variables or directly in the client file during development). - - - ### Create `stack/client.ts` file - - Create a new file `stack/client.ts` in your root directory and fill it with the following Stack app initialization code: - - ```tsx title="stack/client.ts" - import { StackClientApp } from "@stackframe/react"; - // If you use React Router, uncomment the next line and the redirectMethod below - // import { useNavigate } from "react-router-dom"; - - export const stackClientApp = new StackClientApp({ - // You should store these in environment variables based on your project setup - projectId: "your-project-id", - publishableClientKey: "your-publishable-client-key", - tokenStore: "cookie", - // redirectMethod: { useNavigate }, // Optional: only if using react-router-dom - }); - ``` - - - ### Update `App.tsx` - - If you're using React Router, update your `App.tsx` file to wrap the entire app with a `StackProvider` and `StackTheme` and add a `StackHandler` route to handle the authentication flow. - - ```tsx title="App.tsx" - import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; - import { Suspense } from "react"; - import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; - import { stackClientApp } from "./stack/client"; - - function HandlerRoutes() { - const location = useLocation(); - return ( - - ); - } - - export default function App() { - return ( - - - - - - } /> - hello world
    } /> - - - - - - ); - } - ``` + + For Next.js, add a Suspense boundary: + + + Loading... + ; +}`, + language: "typescript", + filename: "app/loading.tsx" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js" }} + title="Loading Component" + /> ### Done! - That's it! Stack is now configured in your React project. If you start your app and navigate to [http://localhost:5173/handler/sign-up](http://localhost:5173/handler/sign-up), you will see the sign-up page. - -
    - SignIn -
    - - After signing up/in, you will be redirected back to the home page. You can also check out the [http://localhost:5173/handler/account-settings](http://localhost:5173/handler/account-settings) page. +## Post-setup -{/* ELSE_IF_PLATFORM js */} +That's it! Stack is now configured in your project. - -Welcome to the JavaScript SDK setup guide. If you're looking for guides for other frameworks, check out the [React SDK Setup](/docs/react/getting-started/setup), or the [Next.js SDK Setup](/docs/next/getting-started/setup). - +### Testing your setup -Before getting started, make sure you have a JavaScript project set up (such as Node.js, Vite, or any other JavaScript framework). + - - - Setup wizard (recommended) - Manual installation - - - - - - ### Run installation wizard - - Run Stack's installation wizard with the following command: +### What you'll see - ```sh title="Terminal" - npx @stackframe/init-stack@latest - ``` +For JavaScript frameworks with built-in UI components, you'll see the Stack Auth sign-up page: - - ### Update API keys - - Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its values into the `stack/server.ts` or `stack/client.ts` file. - - - - Server - Client - - - - ```tsx title="stack/server.ts" - import { StackServerApp } from "@stackframe/js"; - - export const stackServerApp = new StackServerApp({ - // You should store these in environment variables based on your project setup - projectId: "your-project-id", - publishableClientKey: "your-publishable-client-key", - secretServerKey: "your-secret-server-key", - tokenStore: "memory", - }); - ``` - - - - ```tsx title="stack/client.ts" - import { StackClientApp } from "@stackframe/js"; - - export const stackClientApp = new StackClientApp({ - // You should store these in environment variables based on your project setup - projectId: "your-project-id", - publishableClientKey: "your-publishable-client-key", - tokenStore: "cookie", - }); - ``` - - - - - - - - - ### Install npm package - +
    + SignIn +
    - ```bash title="Terminal" - npm install @stackframe/js - ``` +After signing up/in, you will be redirected back to the home page. You can also check out the account settings page. - - ### Update API keys - - Then, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its values into the `stack/server.ts` or `stack/client.ts` file. +![Stack account settings page](/imgs/account-settings.png) - - ### Initialize the app - - - - Server - Client - - - - ```typescript title="stack/server.ts" - import { StackServerApp } from "@stackframe/js"; - - const stackServerApp = new StackServerApp({ - // You should store these in environment variables based on your project setup - projectId: "your-project-id-from-dashboard", - publishableClientKey: "your-publishable-client-key-from-dashboard", - secretServerKey: "your-secret-server-key-from-dashboard", - tokenStore: "memory", - }); - ``` - - - - ```tsx title="stack/client.ts" - import { StackClientApp } from "@stackframe/js"; - - const stackClientApp = new StackClientApp({ - // You should store these in environment variables based on your project setup - projectId: "your-project-id", - publishableClientKey: "your-publishable-client-key", - tokenStore: "cookie", - }); - ``` - - -
    -
    -
    +For Python and backend-only JavaScript setups, you'll interact with Stack Auth through the REST API. ## Example usage - - - Server - Client - +Here are some basic usage examples for each platform: + + - ```typescript - import { stackServerApp } from "@/stack/server"; - - const user = await stackServerApp.getUser("user_id"); - - await user.update({ - displayName: "New Display Name", - }); - - const team = await stackServerApp.createTeam({ - name: "New Team", - }); + if (user) { + return
    Hello, {user.displayName}!
    ; + } else { + return
    Please sign in
    ; + } +}`, + language: "typescript", + filename: "Client Component" + } + }, + "React": { + code: `import { useUser } from "@stackframe/react"; - await team.addUser(user.id); - ``` - +export default function MyComponent() { + const user = useUser(); - - ```typescript - import { stackClientApp } from "@/stack/client"; - - await stackClientApp.signInWithCredential({ - email: "test@example.com", - password: "password123", - }); - - const user = await stackClientApp.getUser(); - - await user.update({ - displayName: "New Display Name", - }); - - await user.signOut(); - ``` - -
    - -## Next steps - -Check out the [Users](./users.mdx) to learn how to retrieve and update user information, or [Example pages](./example-pages.mdx) to see how to build your sign-in/up pages. - - -{/* ELSE_IF_PLATFORM python */} - - -Welcome to the Python setup guide. If you're looking for guides for other frameworks, check out the [Next.js SDK Setup](/next/getting-started/setup), [React SDK Setup](/react/getting-started/setup), or the [JavaScript SDK Setup](/js/getting-started/setup). - - -Our recommended way to use Stack Auth with Python is with the [REST API](../rest-api/overview.mdx). It provides a fully documented way to interact with Stack Auth from any Python framework, including Flask, FastAPI, and Django. - -For the purpose of this guide, we will use the `requests` library to make HTTP requests to the Stack Auth API. If you haven't already, you can install it in your environment with `pip install requests`. - - - ### Create API keys - - First, create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), and copy your project ID, publishable client key, and secret server key into a safe place (eg. environment variables). - - From there, you can access them in your Python code. You can then read them like this: - - ```python - import os - - stack_project_id = os.getenv("STACK_PROJECT_ID") - stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") - stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY") - ``` - - ### Make a request - - Next, create a helper function to make requests to the Stack Auth API: - - ```python - import requests - - def stack_auth_request(method, endpoint, **kwargs): - res = requests.request( - method, - f'https://api.stack-auth.com/{endpoint}', - headers={ - 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' - **kwargs.pop('headers', {}), + if (user) { + return
    Hello, {user.displayName}!
    ; + } else { + return
    Please sign in
    ; + } +}`, + language: "typescript", + filename: "Component" }, - **kwargs, - ) - if res.status_code >= 400: - raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") - return res.json() + "Express": { + code: `import { stackServerApp } from "./stack/server.js"; + +app.get('/profile', async (req, res) => { + try { + // Get access token from request headers + const accessToken = req.headers['x-stack-access-token']; + const user = await stackServerApp.getUser({ accessToken }); + + if (user) { + res.json({ message: \`Hello, \${user.displayName}!\` }); + } else { + res.status(401).json({ error: 'Not authenticated' }); + } + } catch (error) { + res.status(500).json({ error: 'Server error' }); + } +});`, + language: "typescript", + filename: "server.ts" + }, + "Node.js": { + code: `import { stackServerApp } from "./stack/server.js"; - print(stack_auth_request('GET', '/api/v1/projects/current')) - ``` - ### Retrieve the access tokens +async function checkUser(accessToken) { + try { + const user = await stackServerApp.getUser({ accessToken }); + + if (user) { + console.log(\`Hello, \${user.displayName}!\`); + } else { + console.log('User not authenticated'); + } + } catch (error) { + console.error('Error:', error); + } +}`, + language: "javascript", + filename: "index.js" + } + }, + "Python": { + "Django": { + code: `# In your views.py +def profile_view(request): + # Get access token from request headers + access_token = request.headers.get('X-Stack-Access-Token') + + try: + user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ + 'x-stack-access-token': access_token, + }) + return JsonResponse({'message': f"Hello, {user_data['displayName']}!"}) + except Exception as e: + return JsonResponse({'error': 'Not authenticated'}, status=401)`, + language: "python", + filename: "views.py" + }, + "FastAPI": { + code: `from fastapi import FastAPI, Header, HTTPException - If you're building a backend server, most likely you'll want to use the currently signed in user's access token. Most normally, you would send this with all your requests to the backend in an HTTP header. +app = FastAPI() - In Stack Auth's JavaScript SDK, you can retrieve the access token [from the `stackClientApp` object](/sdk/types/user#currentusergetauthjson). Then, you can use said access token to make requests to Stack Auth: +@app.get("/profile") +async def get_profile(x_stack_access_token: str = Header(None)): + if not x_stack_access_token: + raise HTTPException(status_code=401, detail="Access token required") + + try: + user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ + 'x-stack-access-token': x_stack_access_token, + }) + return {"message": f"Hello, {user_data['displayName']}!"} + except Exception as e: + raise HTTPException(status_code=401, detail="Not authenticated")`, + language: "python", + filename: "main.py" + }, + "Flask": { + code: `from flask import Flask, request, jsonify - ```python - access_token = # access token retrieved from the JavaScript SDK +app = Flask(__name__) - print(stack_auth_request('GET', '/api/v1/users/me', headers={ +@app.route('/profile') +def profile(): + access_token = request.headers.get('X-Stack-Access-Token') + + if not access_token: + return jsonify({'error': 'Access token required'}), 401 + + try: + user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ 'x-stack-access-token': access_token, - })) - ``` - - ### Done! -
    + }) + return jsonify({'message': f"Hello, {user_data['displayName']}!"}) + except Exception as e: + return jsonify({'error': 'Not authenticated'}), 401`, + language: "python", + filename: "app.py" + } + } + }} + defaultPlatform="JavaScript" + defaultFrameworks={{ "JavaScript": "Next.js", "Python": "Django" }} + title="Basic Usage Examples" +/> ## Next steps -Check out the [REST API documentation](../rest-api/overview.mdx) to learn more about the available endpoints and how to use them in your Python application. - -{/* END_PLATFORM */} +Next up, we will show you how to [retrieve and update user information](./users.mdx), and how to [protect a page](./users.mdx#protecting-a-page) from unauthorized access. +For Python developers, check out the [REST API documentation](../rest-api/overview.mdx) to learn more about the available endpoints and how to use them in your Python application. From 98ef9a35f778ba597e82567fb1dc973a0b3870ab Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 01:54:59 -0500 Subject: [PATCH 14/55] fix docs-tree --- docs/src/lib/docs-tree.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/lib/docs-tree.ts b/docs/src/lib/docs-tree.ts index 9b5469ac3b..8719dbcb25 100644 --- a/docs/src/lib/docs-tree.ts +++ b/docs/src/lib/docs-tree.ts @@ -1,3 +1,4 @@ +import { throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import type { PageTree } from 'fumadocs-core/server'; export type DocsSection = 'guides' | 'sdk' | 'components'; @@ -100,7 +101,7 @@ function matchesSection(url: string, section: DocsSection): boolean { } function normalizeUrl(url: string): string { - const withoutFragment = url.split('#')[0]; + const withoutFragment = url.split('#')[0] ?? throwErr("URL split by # returned empty array", { url }); return withoutFragment.replace(/\/$/, ''); } @@ -142,7 +143,7 @@ function flattenRootChildren(nodes: PageTree.Node[]): PageTree.Node[] { return nodes; } - const soleNode = nodes[0] as PageTree.Node; + const soleNode = nodes[0] ?? throwErr("Expected at least one node but array is empty", { nodesLength: nodes.length }); if (soleNode.type !== 'folder') { return nodes; } From fc3a382b7694de1f970b36178839dc2f683eac25 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 01:55:19 -0500 Subject: [PATCH 15/55] fix tab detection in header --- docs/src/components/layouts/shared-header.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/src/components/layouts/shared-header.tsx b/docs/src/components/layouts/shared-header.tsx index 387c1e82aa..f1ba1571c3 100644 --- a/docs/src/components/layouts/shared-header.tsx +++ b/docs/src/components/layouts/shared-header.tsx @@ -28,17 +28,15 @@ type SharedHeaderProps = { * Helper functions to detect which section we're in */ export function isInSdkSection(pathname: string): boolean { - // Match the actual SDK section: /docs/platform/sdk or /docs/platform/sdk/... + // Match the actual SDK section: /docs/sdk or /docs/sdk/... // This excludes docs pages that might mention SDK in other contexts - const match = pathname.match(/^\/docs\/[^\/]+\/sdk($|\/)/); - return Boolean(match); + return pathname === '/docs/sdk' || pathname.startsWith('/docs/sdk/'); } export function isInComponentsSection(pathname: string): boolean { - // Match the actual Components section: /docs/platform/components or /docs/platform/components/... - // This excludes docs pages like /docs/platform/getting-started/components - const match = pathname.match(/^\/docs\/[^\/]+\/components($|\/)/); - return Boolean(match); + // Match the actual Components section: /docs/components or /docs/components/... + // This excludes docs pages that might mention components in other contexts + return pathname === '/docs/components' || pathname.startsWith('/docs/components/'); } export function isInApiSection(pathname: string): boolean { From 3ee4ba8039585b2c11e3e414629914998e240b63 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 01:56:02 -0500 Subject: [PATCH 16/55] Update examples for hook usage --- docs/content/docs/(guides)/concepts/api-keys.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/(guides)/concepts/api-keys.mdx b/docs/content/docs/(guides)/concepts/api-keys.mdx index 146abe7a39..79528f2b67 100644 --- a/docs/content/docs/(guides)/concepts/api-keys.mdx +++ b/docs/content/docs/(guides)/concepts/api-keys.mdx @@ -177,8 +177,14 @@ To use API keys in your application, you need to enable them in your project set const teamApiKeys = await team.listApiKeys(); // Using hooks in React components - const apiKeys = user.useApiKeys(); - const teamApiKeys = team.useApiKeys(); + function ApiKeysList() { + const user = useUser(); + const apiKeys = user.useApiKeys(); + const team = user.getTeam("team-id-here"); + const teamApiKeys = team.useApiKeys(); + + // Render UI here. + } ``` From 004089da2705b0d477aff5bd8e444a00df8d034f Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 01:56:56 -0500 Subject: [PATCH 17/55] fix callback url example --- .../content/docs/(guides)/concepts/auth-providers/x-twitter.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx b/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx index a583febbca..43880d8bce 100644 --- a/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx +++ b/docs/content/docs/(guides)/concepts/auth-providers/x-twitter.mdx @@ -23,7 +23,7 @@ This guide explains how to set up X (formerly Twitter) as an authentication prov 10. Click **Set up** or **Edit** if already configured. 11. Enable **OAuth 2.0** and set the following details: - **Type of App**: Web App - - **Callback URL / Redirect URL**: `https://api.stack-auth.com/api/latest/auth/oauth/callback/x` + - **Callback URL / Redirect URL**: `https://api.stack-auth.com/api/v1/auth/oauth/callback/x` - **Website URL**: Your website's URL 12. Under **App permissions**, select your scopes. 13. Click **Save** to apply your changes. From 4ab88c9cbb5974d60874580baa7358b08801aac5 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 01:58:19 -0500 Subject: [PATCH 18/55] fix audience param value missing quotes --- docs/content/docs/(guides)/concepts/backend-integration.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/concepts/backend-integration.mdx b/docs/content/docs/(guides)/concepts/backend-integration.mdx index e91d035d64..86afc10707 100644 --- a/docs/content/docs/(guides)/concepts/backend-integration.mdx +++ b/docs/content/docs/(guides)/concepts/backend-integration.mdx @@ -77,7 +77,7 @@ Stack Auth provides two methods for authenticating users on your server endpoint access_token, signing_key.key, algorithms=["ES256"], - audience= + audience="" ) print('Authenticated user with ID:', payload['sub']) except Exception as error: From f328228b62a2fc38444c2a7eb791805bd982b661 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:00:47 -0500 Subject: [PATCH 19/55] clientReadonlyMetadata -> clientReadOnlyMetadata --- docs/content/docs/(guides)/getting-started/users.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/getting-started/users.mdx b/docs/content/docs/(guides)/getting-started/users.mdx index a41364f8f2..17ecb67c70 100644 --- a/docs/content/docs/(guides)/getting-started/users.mdx +++ b/docs/content/docs/(guides)/getting-started/users.mdx @@ -138,7 +138,7 @@ export default function MyClientComponent() { } ``` -You can also store custom user data in the `clientMetadata`, `serverMetadata`, or `clientReadonlyMetadata` fields. More information [here](../concepts/custom-user-data). +You can also store custom user data in the `clientMetadata`, `serverMetadata`, or `clientReadOnlyMetadata` fields. More information [here](../concepts/custom-user-data). ## Signing out From 6842815098f8af29edbd46cd05cc5eb13941d63a Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:02:41 -0500 Subject: [PATCH 20/55] clientReadonlyMetadata -> clientReadOnlyMetadata --- docs/content/docs/(guides)/concepts/user-onboarding.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/concepts/user-onboarding.mdx b/docs/content/docs/(guides)/concepts/user-onboarding.mdx index c451e1575f..1ecf058867 100644 --- a/docs/content/docs/(guides)/concepts/user-onboarding.mdx +++ b/docs/content/docs/(guides)/concepts/user-onboarding.mdx @@ -47,7 +47,7 @@ export default function OnboardingPage() { ``` -While the above implementation offers a basic onboarding process, users can still skip onboarding by directly sending an API request to update the `clientMetadata.onboarded` flag. If you want to ensure that onboarding cannot be bypassed on the API level, you should create a server endpoint to validate and store the data, then save the `onboarded` flag in the `clientReadonlyMetadata` on the server side after validation. +While the above implementation offers a basic onboarding process, users can still skip onboarding by directly sending an API request to update the `clientMetadata.onboarded` flag. If you want to ensure that onboarding cannot be bypassed on the API level, you should create a server endpoint to validate and store the data, then save the `onboarded` flag in the `clientReadOnlyMetadata` on the server side after validation. Next, we can create a hook/function to check if the user has completed onboarding and redirect them to the onboarding page: From 4b78dbcf8e5a4682e478da61c5f382cf4b489118 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:04:46 -0500 Subject: [PATCH 21/55] update examples in user-onboarding --- docs/content/docs/(guides)/concepts/user-onboarding.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/(guides)/concepts/user-onboarding.mdx b/docs/content/docs/(guides)/concepts/user-onboarding.mdx index 1ecf058867..a2edfd4a1c 100644 --- a/docs/content/docs/(guides)/concepts/user-onboarding.mdx +++ b/docs/content/docs/(guides)/concepts/user-onboarding.mdx @@ -69,7 +69,7 @@ Next, we can create a hook/function to check if the user has completed onboardin const router = useRouter(); useEffect(() => { - if (!user.clientMetadata.onboarded) { + if (!user.clientReadOnlyMetadata.onboarded) { router.push('/onboarding'); } }, [user]); @@ -83,7 +83,7 @@ Next, we can create a hook/function to check if the user has completed onboardin export async function ensureOnboarded() { const user = await stackServerApp.getUser(); - if (!user.serverMetadata.onboarded) { + if (!user.clientReadOnlyMetadata.onboarded) { redirect('/onboarding'); } } From 28c2b33a02ffd04931bf98d97da9fa461450ea67 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:06:24 -0500 Subject: [PATCH 22/55] fix mistake in examples --- docs/content/docs/(guides)/concepts/user-onboarding.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/concepts/user-onboarding.mdx b/docs/content/docs/(guides)/concepts/user-onboarding.mdx index a2edfd4a1c..bc3f5ebac4 100644 --- a/docs/content/docs/(guides)/concepts/user-onboarding.mdx +++ b/docs/content/docs/(guides)/concepts/user-onboarding.mdx @@ -64,7 +64,7 @@ Next, we can create a hook/function to check if the user has completed onboardin import { useUser } from '@stackframe/stack'; import { useRouter } from 'next/navigation'; - export function useOnboarded() { + export function useOnboarding() { const user = useUser(); const router = useRouter(); From 7972f362320ea0119b3f552f4fa8a616314e0ed6 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:08:04 -0500 Subject: [PATCH 23/55] Update webhooks examples --- docs/content/docs/(guides)/concepts/webhooks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/concepts/webhooks.mdx b/docs/content/docs/(guides)/concepts/webhooks.mdx index 320cbfe474..67c59757bb 100644 --- a/docs/content/docs/(guides)/concepts/webhooks.mdx +++ b/docs/content/docs/(guides)/concepts/webhooks.mdx @@ -44,7 +44,7 @@ const payload = ""; const wh = new Webhook(secret); // Throws on error, returns the verified content on success -const payload = wh.verify(payload, headers); +const verifiedPayload = wh.verify(payload, headers); ``` If you do not want to install the Svix client library or are using a language that is not supported, you can [verify the signature manually](https://docs.svix.com/receiving/verifying-payloads/how-manual). From 93eaa1a46c25e787112d5096f766800d2c84ec06 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:12:24 -0500 Subject: [PATCH 24/55] update dark-mode examples --- .../docs/(guides)/customization/dark-mode.mdx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/(guides)/customization/dark-mode.mdx b/docs/content/docs/(guides)/customization/dark-mode.mdx index a0381abd76..18c0ef320f 100644 --- a/docs/content/docs/(guides)/customization/dark-mode.mdx +++ b/docs/content/docs/(guides)/customization/dark-mode.mdx @@ -14,12 +14,14 @@ Here is an example of how to set up next-themes with Stack (find more details in npm install next-themes ``` -2. Add the `ThemeProvider` to your `layout.tsx` file: +2. Create a client-side provider component: - ```jsx + ```jsx title="components/providers.jsx" + 'use client'; import { ThemeProvider } from 'next-themes' + import { StackTheme } from '@stackframe/stack' - export default function Layout({ children }) { + export default function Providers({ children }) { return ( {/* ThemeProvider enables theme switching throughout the application. @@ -36,7 +38,25 @@ Here is an example of how to set up next-themes with Stack (find more details in } ``` -3. Build a color mode switcher component: +3. Use the provider in your `layout.tsx` file: + + ```jsx title="app/layout.tsx" + import Providers from './components/providers' + + export default function Layout({ children }) { + return ( + + + + {children} + + + + ) + } + ``` + +4. Build a color mode switcher component: ```jsx 'use client'; From 2d8bca171671b6ab84cdbae7919ceb8980f2d32d Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:24:01 -0500 Subject: [PATCH 25/55] Update forgot-password examples --- .../customization/page-examples/forgot-password.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx b/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx index 7da437e827..98876c2ac1 100644 --- a/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx +++ b/docs/content/docs/(guides)/customization/page-examples/forgot-password.mdx @@ -55,6 +55,7 @@ export default function CustomForgotPasswordForm() { const onSubmit = async () => { if (!email) { setError('Please enter your email address'); + setMessage(''); return; } @@ -63,15 +64,20 @@ export default function CustomForgotPasswordForm() { if (result?.status === 'error') { if (result.error.code === 'user_not_found') { // For security reasons, don't reveal if a user exists or not + setError(''); setMessage('If an account exists with this email, a password reset link has been sent.'); } else { setError(`Error: ${result.error.message}`); + setMessage(''); } } else { + setError(''); setMessage('Password reset email sent! Please check your inbox.'); } } catch (err) { - setError(`An unexpected error occurred: ${err.message}`); + const message = err instanceof Error ? err.message : 'Unknown error'; + setError(`An unexpected error occurred: ${message}`); + setMessage(''); } }; From b7f73c04066fa69b183ca1f2c5a5ad6db9f722e1 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:29:33 -0500 Subject: [PATCH 26/55] update examples-pages --- .../getting-started/example-pages.mdx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/content/docs/(guides)/getting-started/example-pages.mdx b/docs/content/docs/(guides)/getting-started/example-pages.mdx index 913db8b005..5d21db33f4 100644 --- a/docs/content/docs/(guides)/getting-started/example-pages.mdx +++ b/docs/content/docs/(guides)/getting-started/example-pages.mdx @@ -344,28 +344,26 @@ export const stackClientApp = new StackClientApp({ } }); - document.getElementById("signUp")?.addEventListener("click", async () => { - const emailInput = document.getElementById("signUpEmail") as HTMLInputElement; - const passwordInput = document.getElementById("signUpPassword") as HTMLInputElement; - - const result = await stackClientApp.signUpWithCredential({ + document.getElementById("sendCode")?.addEventListener("click", async () => { + const emailInput = document.getElementById("emailInput") as HTMLInputElement; + + await stackClientApp.sendMagicLinkEmail({ email: emailInput.value, - password: passwordInput.value, }); + + document.getElementById("emailStep")!.style.display = "none"; + document.getElementById("codeStep")!.style.display = "block"; + }); - if (result.status === "error") { - alert("Sign up failed. Please try again."); - return; - } - - const signInResult = await stackClientApp.signInWithCredential({ - email: emailInput.value, - password: passwordInput.value, + document.getElementById("verifyCode")?.addEventListener("click", async () => { + const codeInput = document.getElementById("codeInput") as HTMLInputElement; + + const result = await stackClientApp.signInWithMagicLink({ + code: codeInput.value, }); - - if (signInResult.status === "error") { - alert("Account created but sign in failed. Please sign in manually."); - window.location.href = "/password-sign-in"; + + if (result.status === "error") { + alert("Verification failed. Please check the code and try again."); } else { window.location.href = "/"; } From 1a3d85fa7b9c9fcaca8f3b7650b67daee05c4847 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:30:15 -0500 Subject: [PATCH 27/55] proper git link --- docs/content/docs/(guides)/others/self-host.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/docs/(guides)/others/self-host.mdx b/docs/content/docs/(guides)/others/self-host.mdx index 76411e87d0..c2321269cf 100644 --- a/docs/content/docs/(guides)/others/self-host.mdx +++ b/docs/content/docs/(guides)/others/self-host.mdx @@ -60,7 +60,7 @@ Now, login with your admin account on the dashboard and follow the [normal setup Clone the repository and check out the directory: ```sh -git clone git@github.com:stack-auth/stack.git +git clone git@github.com:stack-auth/stack-auth.git cd stack ``` @@ -109,7 +109,7 @@ Deploy these services with your preferred platform. Copy the URLs/API keys—you Clone the repository and check out the root directory: ```sh -git clone git@github.com:stack-auth/stack.git +git clone git@github.com:stack-auth/stack-auth.git cd stack ``` @@ -128,7 +128,7 @@ pnpm start:backend Clone the repository (if you are running it on a separate server, or skip this step if you are using the same server as the API backend) and check out the dashboard directory: ```sh -git clone git@github.com:stack-auth/stack.git +git clone git@github.com:stack-auth/stack-auth.git cd stack ``` From a9ad25c1c18aab302ce822f0b1af72b49a3a6301 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:36:13 -0500 Subject: [PATCH 28/55] distinguish between client/server headers --- .../docs/(guides)/rest-api/overview.mdx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/content/docs/(guides)/rest-api/overview.mdx b/docs/content/docs/(guides)/rest-api/overview.mdx index 1a85e84bb7..44127cf235 100644 --- a/docs/content/docs/(guides)/rest-api/overview.mdx +++ b/docs/content/docs/(guides)/rest-api/overview.mdx @@ -8,24 +8,44 @@ Stack offers a REST API for backends & frontends of any programming language or ## Authentication -The following authentication headers are common to every endpoint: +Stack Auth uses different authentication patterns depending on whether you're making requests from client-side code (browser, mobile app) or server-side code (your backend). + + +**Security Critical**: Never expose your secret server key (`ssk_...`) in client-side code, browser requests, or any publicly accessible location. Server keys should only be used in secure backend environments. + + +### Client-Side Authentication + +For requests from browsers, mobile apps, or other client-side environments: ```http curl https://api.stack-auth.com/api/v1/ \ - -H "X-Stack-Access-Type: " \ + -H "X-Stack-Access-Type: client" \ -H "X-Stack-Project-Id: " \ -H "X-Stack-Publishable-Client-Key: pck_" \ - -H "X-Stack-Secret-Server-Key: ssk_" \ -H "X-Stack-Access-Token: " ``` -| Header | Type | Description | -| ------ | ---- | ----------- | -| `X-Stack-Access-Type` | "client" \| "server" | Required. "client" (without quotes) for the frontend API, or "server" for the backend API. | -| `X-Stack-Project-Id` | UUID | Required. The project ID as found on the Stack dashboard. | -| `X-Stack-Publishable-Client-Key` | string | Required for client access. The API key as found on the Stack dashboard. | -| `X-Stack-Secret-Server-Key` | string | Required for server access. The API key as found on the Stack dashboard. | -| `X-Stack-Access-Token` | string | Optional. The access token for the current user. If not given, the request is considered to be logged out. | +### Server-Side Authentication + +For requests from your secure backend server: + +```http +curl https://api.stack-auth.com/api/v1/ \ + -H "X-Stack-Access-Type: server" \ + -H "X-Stack-Project-Id: " \ + -H "X-Stack-Secret-Server-Key: ssk_" +``` + +### Authentication Headers + +| Header | Type | Used In | Description | +| ------ | ---- | ------- | ----------- | +| `X-Stack-Access-Type` | "client" \| "server" | Both | Required. Use "client" for frontend/browser requests, "server" for backend requests. | +| `X-Stack-Project-Id` | UUID | Both | Required. Your project ID from the Stack dashboard. | +| `X-Stack-Publishable-Client-Key` | string | Client only | Required for client access. Safe to expose in frontend code. Starts with `pck_`. | +| `X-Stack-Secret-Server-Key` | string | Server only | Required for server access. **Never expose in client code**. Starts with `ssk_`. | +| `X-Stack-Access-Token` | string | Client only | Optional. The current user's access token. Used to act on behalf of a specific user. | {/* IF_PLATFORM python */} @@ -60,9 +80,11 @@ To see how to use these headers in various programming languages, see the [examp - **Client access type** is mostly used for client-side applications, like a browser or mobile app. The client APIs can only read and update the currently authenticated user's data, and it is usually fine to post the publishable client key in the client-side code. + **Client access type** (`X-Stack-Access-Type: client`) is for client-side applications like browsers and mobile apps. Client APIs can only read and update the currently authenticated user's data. Use your publishable client key (`pck_...`) - it's safe to include in frontend code. + + **Server access type** (`X-Stack-Access-Type: server`) is for your secure backend server. It has full access over all user data using your secret server key (`ssk_...`). - **Server access type**, on the other hand, is for your backend server that you control. It has full access over all user data, and the secret server key should never be exposed to client-side code. + **🚨 Security Warning**: Never use server access type or secret server keys in client-side code, browser requests, or any publicly accessible location. Always keep server keys secure on your backend. For more information, see the concept documentation on [StackApp](../concepts/stack-app#client-vs-server). From 8a45774e716e97360f7e917122555ecbc7411610 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 8 Oct 2025 02:51:39 -0500 Subject: [PATCH 29/55] minor changes --- docs/content/docs/components/forgot-password.mdx | 2 +- docs/content/docs/components/selected-team-switcher.mdx | 3 --- docs/content/docs/sdk/types/project.mdx | 2 +- docs/src/components/layout/custom-search-dialog.tsx | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/content/docs/components/forgot-password.mdx b/docs/content/docs/components/forgot-password.mdx index 1d202bc615..4a8a102e0d 100644 --- a/docs/content/docs/components/forgot-password.mdx +++ b/docs/content/docs/components/forgot-password.mdx @@ -11,7 +11,7 @@ Renders a forgot password component with options for full-page display. { name: "fullPage", type: "boolean", - description: "If set to 'true'. displays the component in full-page mode.", + description: "If true, displays the component in full-page mode.", optional: true, default: "false" } diff --git a/docs/content/docs/components/selected-team-switcher.mdx b/docs/content/docs/components/selected-team-switcher.mdx index b9bdaf684d..2136796a3b 100644 --- a/docs/content/docs/components/selected-team-switcher.mdx +++ b/docs/content/docs/components/selected-team-switcher.mdx @@ -29,9 +29,6 @@ For a comprehensive guide on using this component, refer to our [Team Selection optional: true, default: "false", }, - { - name: "mockUser", - } ]} /> diff --git a/docs/content/docs/sdk/types/project.mdx b/docs/content/docs/sdk/types/project.mdx index cbb2079f87..ffadb921c2 100644 --- a/docs/content/docs/sdk/types/project.mdx +++ b/docs/content/docs/sdk/types/project.mdx @@ -5,7 +5,7 @@ full: true The `Project` object contains the information and configuration of a project, such as the name, description, and enabled authentication methods. -Each [Stack app](../../concepts/stack-app.mdx) corresponds to a project. You can obtain its `Project` object by calling [`stackApp.getProject()`](../objects/stack-app.mdx#stackappgetproject) +Each [Stack app](../../concepts/stack-app) corresponds to a project. You can obtain its `Project` object by calling [`stackApp.getProject()`](../objects/stack-app.mdx#stackappgetproject) or [`stackApp.useProject()`](../objects/stack-app.mdx#stackappuseproject) {/* THIS_LINE_PLATFORM react-like */} . diff --git a/docs/src/components/layout/custom-search-dialog.tsx b/docs/src/components/layout/custom-search-dialog.tsx index 08749c8004..a3abf97379 100644 --- a/docs/src/components/layout/custom-search-dialog.tsx +++ b/docs/src/components/layout/custom-search-dialog.tsx @@ -20,7 +20,7 @@ type GroupedResult = { function extractBasePathFromUrl(url: string): string { // Extract everything after the platform but before any hash - const match = url.match(/\/docs\/(?:[^\/]+\/)?(.+?)(?:#|$)/); + const match = url.match(/\/docs\/(.+?)(?:#|$)/); return match?.[1] || ''; } From 8ec5bd8a542097d3d8d6ee4709c4793c3c0a0264 Mon Sep 17 00:00:00 2001 From: Madison Date: Sun, 12 Oct 2025 01:27:46 -0500 Subject: [PATCH 30/55] remove overview-new temp file --- docs/content/docs/sdk/overview-new.mdx | 51 -------------------------- 1 file changed, 51 deletions(-) delete mode 100644 docs/content/docs/sdk/overview-new.mdx diff --git a/docs/content/docs/sdk/overview-new.mdx b/docs/content/docs/sdk/overview-new.mdx deleted file mode 100644 index 8609f00b48..0000000000 --- a/docs/content/docs/sdk/overview-new.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: SDK Overview ---- - - -This is the SDK reference for Stack Auth's Next.js SDK. - -For a list of components, see the [Components](../components) page. For instructions on how to get started and how to use the SDK, see the [Setup & Installation](../getting-started/setup.mdx) page. If you are using a framework or programming language other than Next.js, you can use [our REST API](../rest-api). - -export const sdkSections = [ - { - title: "General", - items: [ - { name: "StackClientApp", href: "./objects/stack-app#stackclientapp", icon: "object" }, - { name: "StackServerApp", href: "./sdk/objects/stack-app#stackserverapp", icon: "object" }, - { name: "Project", href: "./sdk/types/project#project", icon: "type" }, - ] - }, - { - title: "Users & user data", - items: [ - { name: "CurrentUser", href: "./sdk/types/user#currentuser", icon: "type" }, - { name: "ServerUser", href: "./sdk/types/user#serveruser", icon: "type" }, - { name: "CurrentServerUser", href: "./sdk/types/user#currentserveruser", icon: "type" }, - { name: "ContactChannel", href: "./sdk/types/contact-channel#contactchannel", icon: "type" }, - { name: "ServerContactChannel", href: "./sdk/types/contact-channel#servercontactchannel", icon: "type" }, - ] - }, - { - title: "Teams", - items: [ - { name: "Team", href: "./sdk/types/team#team", icon: "type" }, - { name: "ServerTeam", href: "./sdk/types/team#serverteam", icon: "type" }, - { name: "TeamPermission", href: "./sdk/types/team-permission#teampermission", icon: "type" }, - { name: "ServerTeamPermission", href: "./sdk/types/team-permission#serverteampermission", icon: "type" }, - { name: "TeamUser", href: "./sdk/types/team-user#teamuser", icon: "type" }, - { name: "ServerTeamUser", href: "./sdk/types/team-user#serverteamuser", icon: "type" }, - { name: "TeamProfile", href: "./sdk/types/team-profile#teamprofile", icon: "type" }, - { name: "ServerTeamProfile", href: "./sdk/types/team-profile#serverteamprofile", icon: "type" }, - ] - }, - { - title: "Hooks", - items: [ - { name: "useStackApp", href: "./sdk/hooks/use-stack-app", icon: "hook" }, - { name: "useUser", href: "./sdk/hooks/use-user", icon: "hook" }, - ] - } -]; - - From 0910c9c5c89d58904a738ed74c003a7ddc963d5c Mon Sep 17 00:00:00 2001 From: Madison Date: Sun, 12 Oct 2025 23:10:37 -0500 Subject: [PATCH 31/55] api page styling --- docs/src/app/api/[[...slug]]/page.tsx | 7 +- docs/src/app/api/layout.tsx | 53 +++--- .../components/layouts/api/api-sidebar.tsx | 171 +++++++++--------- docs/src/components/layouts/shared-header.tsx | 4 +- 4 files changed, 120 insertions(+), 115 deletions(-) diff --git a/docs/src/app/api/[[...slug]]/page.tsx b/docs/src/app/api/[[...slug]]/page.tsx index 3a71abd9dd..817e746c3f 100644 --- a/docs/src/app/api/[[...slug]]/page.tsx +++ b/docs/src/app/api/[[...slug]]/page.tsx @@ -2,7 +2,6 @@ import { EnhancedAPIPage } from '@/components/api/enhanced-api-page'; import { getMDXComponents } from '@/mdx-components'; import { apiSource } from 'lib/source'; import { redirect } from 'next/navigation'; -import { SharedContentLayout } from '../../../components/layouts/shared-content-layout'; export default async function ApiPage({ params, @@ -16,9 +15,5 @@ export default async function ApiPage({ const MDX = page.data.body; - return ( - - - - ); + return ; } diff --git a/docs/src/app/api/layout.tsx b/docs/src/app/api/layout.tsx index 1065814116..65870727c3 100644 --- a/docs/src/app/api/layout.tsx +++ b/docs/src/app/api/layout.tsx @@ -3,6 +3,7 @@ import { AuthPanel } from '@/components/api/auth-panel'; import { AIChatDrawer } from '@/components/chat/ai-chat'; import { ApiSidebar } from '@/components/layouts/api/api-sidebar-server'; import { DocsHeaderWrapper } from '@/components/layouts/docs-header-wrapper'; +import { SharedContentLayout } from '@/components/layouts/shared-content-layout'; import { SidebarProvider } from '@/components/layouts/sidebar-context'; import { apiSource } from '../../../lib/source'; @@ -90,33 +91,39 @@ export default function ApiLayout({ children }: { children: React.ReactNode }) { return ( -
    - {/* Full-width header with Stack Auth branding */} - - -
    - {/* Custom API Sidebar - positioned under header, hidden on mobile */} -
    - -
    - - {/* Main content area - responsive margin based on sidebar state */} -
    - {/* Page content */} -
    - {children} +
    +
    + {/* Full-width header with Stack Auth branding */} + + + {/* Main layout container with centered content */} +
    +
    + {/* Custom API Sidebar - positioned under header, hidden on mobile */} +
    +
    + +
    +
    + + {/* Main content area */} +
    + + {children} + +
    -
    - {/* AI Chat Drawer */} - + {/* AI Chat Drawer */} + - {/* Auth Panel */} - + {/* Auth Panel */} + +
    diff --git a/docs/src/components/layouts/api/api-sidebar.tsx b/docs/src/components/layouts/api/api-sidebar.tsx index 5d5f97b6d9..389fdbc876 100644 --- a/docs/src/components/layouts/api/api-sidebar.tsx +++ b/docs/src/components/layouts/api/api-sidebar.tsx @@ -835,97 +835,100 @@ export function ApiSidebarContent({ pages = [] }: { pages?: PageData[] }) { return ( -
    - - - - Overview - - - {isMainSidebarCollapsed ? ( + <> + {/* Scrollable content area */} +
    + + + + Overview + + + {isMainSidebarCollapsed ? ( // Collapsed view - hierarchical dots -
    -
    - {hierarchicalItems.map((item, index) => ( - - ))} -
    -
    - ) : ( - // Expanded view - original layout - Object.entries(organizedPages) - .filter(([sectionKey]) => sectionKey !== 'admin') - .sort(([aKey], [bKey]) => { - const sectionOrder = ['client', 'server', 'webhooks']; - const aIndex = sectionOrder.indexOf(aKey); - const bIndex = sectionOrder.indexOf(bKey); - if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - // eslint-disable-next-line no-restricted-syntax - return aKey.localeCompare(bKey); - }) - .map(([sectionKey, section]) => ( -
    - {section.title} - - {section.pages.length > 0 && section.pages.map((page: PageData) => ( - - {page.data.title || formatTitle(page.slugs[page.slugs.length - 1])} - +
    +
    + {hierarchicalItems.map((item, index) => ( + ))} - - {Object.entries(section.groups).map(([groupKey, group]: [string, OrganizedGroup]) => ( - - {group.pages.map((page: PageData) => { - const method = getHttpMethod(page); - const title = page.data.title || formatTitle(page.slugs[page.slugs.length - 1]); - - if (sectionKey === 'webhooks') { - return ( - - {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} - {!isMainSidebarCollapsed ? ( -
    - - EVENT - - {title} -
    - ) : ( - '🔔' - )} -
    - ); - } - - return ( +
    +
    + ) : ( + // Expanded view - original layout + Object.entries(organizedPages) + .filter(([sectionKey]) => sectionKey !== 'admin') + .sort(([aKey], [bKey]) => { + const sectionOrder = ['client', 'server', 'webhooks']; + const aIndex = sectionOrder.indexOf(aKey); + const bIndex = sectionOrder.indexOf(bKey); + if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + // eslint-disable-next-line no-restricted-syntax + return aKey.localeCompare(bKey); + }) + .map(([sectionKey, section]) => ( +
    + {section.title} + + {section.pages.length > 0 && section.pages.map((page: PageData) => ( - {title} + {page.data.title || formatTitle(page.slugs[page.slugs.length - 1])} - ); - })} - - ))} -
    - )) - )} - - + ))} + + {Object.entries(section.groups).map(([groupKey, group]: [string, OrganizedGroup]) => ( + + {group.pages.map((page: PageData) => { + const method = getHttpMethod(page); + const title = page.data.title || formatTitle(page.slugs[page.slugs.length - 1]); + + if (sectionKey === 'webhooks') { + return ( + + {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} + {!isMainSidebarCollapsed ? ( +
    + + EVENT + + {title} +
    + ) : ( + '🔔' + )} +
    + ); + } + + return ( + + {title} + + ); + })} +
    + ))} +
    + )) + )} +
    +
    +
    {/* Footer with theme toggle and collapse button */}
    @@ -944,7 +947,7 @@ export function ApiSidebarContent({ pages = [] }: { pages?: PageData[] }) {
    )}
    -
    + ); } diff --git a/docs/src/components/layouts/shared-header.tsx b/docs/src/components/layouts/shared-header.tsx index f1ba1571c3..4d7e736993 100644 --- a/docs/src/components/layouts/shared-header.tsx +++ b/docs/src/components/layouts/shared-header.tsx @@ -261,7 +261,7 @@ export function SharedHeader({
    {/* First row */} -
    +
    {/* Left side - Stack Auth Logo and Navigation */}
    {/* Stack Auth Logo - Always visible */} @@ -314,7 +314,7 @@ export function SharedHeader({
    {/* Second row */} -
    +
    {/* Desktop Navigation Links - Hidden on mobile */}
    {navLinks.map((link, index) => { From 9520e2d8d26695c793b86e1595209c84f3270380 Mon Sep 17 00:00:00 2001 From: Madison Date: Sun, 12 Oct 2025 23:11:37 -0500 Subject: [PATCH 32/55] More styling stuff --- docs/src/app/api/layout.tsx | 17 +++++++------- docs/src/app/global.css | 6 +++++ docs/src/components/api/enhanced-api-page.tsx | 2 +- docs/src/components/layouts/docs.tsx | 2 ++ .../layouts/shared-content-layout.tsx | 22 ++++++++++++++----- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/docs/src/app/api/layout.tsx b/docs/src/app/api/layout.tsx index 65870727c3..316c71e6e9 100644 --- a/docs/src/app/api/layout.tsx +++ b/docs/src/app/api/layout.tsx @@ -98,10 +98,8 @@ export default function ApiLayout({ children }: { children: React.ReactNode }) { showSearch={false} apiPages={apiPages} /> - - {/* Main layout container with centered content */}
    -
    +
    {/* Custom API Sidebar - positioned under header, hidden on mobile */}
    @@ -111,19 +109,20 @@ export default function ApiLayout({ children }: { children: React.ReactNode }) { {/* Main content area */}
    - + {children}
    - {/* AI Chat Drawer */} - - - {/* Auth Panel */} -
    + + {/* AI Chat Drawer */} + + + {/* Auth Panel */} +
    diff --git a/docs/src/app/global.css b/docs/src/app/global.css index 17ced2ec86..3f8e408e2c 100644 --- a/docs/src/app/global.css +++ b/docs/src/app/global.css @@ -199,3 +199,9 @@ button:not(.chat-gradient-active)::before { mask: radial-gradient(circle at 20% 80%, transparent 0%, transparent 100%); transition: mask 0.3s ease-out; } + +/* Ensure EnhancedAPIPage components fill their container */ +.api-content-container { + width: 100%; + max-width: none !important; +} diff --git a/docs/src/components/api/enhanced-api-page.tsx b/docs/src/components/api/enhanced-api-page.tsx index b0e37be7b0..6463ea7d62 100644 --- a/docs/src/components/api/enhanced-api-page.tsx +++ b/docs/src/components/api/enhanced-api-page.tsx @@ -321,7 +321,7 @@ export function EnhancedAPIPage({ document, operations, description }: EnhancedA } return ( -
    +
    {/* Operations */} {operations.map(({ path, method }) => { const operation = spec.paths[path][method.toLowerCase()]; diff --git a/docs/src/components/layouts/docs.tsx b/docs/src/components/layouts/docs.tsx index 28dde58195..b29eec8603 100644 --- a/docs/src/components/layouts/docs.tsx +++ b/docs/src/components/layouts/docs.tsx @@ -33,6 +33,8 @@ * - PageTreeItem: Recursive page tree rendering */ +"use client"; + import Link from 'fumadocs-core/link'; import type { PageTree } from 'fumadocs-core/server'; import { diff --git a/docs/src/components/layouts/shared-content-layout.tsx b/docs/src/components/layouts/shared-content-layout.tsx index 2d8fdae60b..ce7a6f81c5 100644 --- a/docs/src/components/layouts/shared-content-layout.tsx +++ b/docs/src/components/layouts/shared-content-layout.tsx @@ -4,9 +4,12 @@ import { type HTMLAttributes, type ReactNode } from 'react'; import { cn } from '../../lib/cn'; import { useSidebar } from './sidebar-context'; +type SharedContentLayoutVariant = 'default' | 'wide' | 'full'; + export type SharedContentLayoutProps = { children: ReactNode, className?: string, + variant?: SharedContentLayoutVariant, } & HTMLAttributes /** @@ -16,10 +19,23 @@ export type SharedContentLayoutProps = { export function SharedContentLayout({ children, className, + variant = 'default', ...props }: SharedContentLayoutProps) { const { isMainSidebarCollapsed = false } = useSidebar() ?? {}; + const resolvedMaxWidth = (() => { + if (variant === 'full') { + return 'max-w-full'; + } + if (variant === 'wide') { + return isMainSidebarCollapsed ? 'max-w-full' : 'max-w-7xl'; + } + return isMainSidebarCollapsed ? 'max-w-full' : 'max-w-6xl'; + })(); + + const baseContainerClasses = 'container mx-auto px-4 md:px-6 py-8 w-full min-w-0 transition-all duration-300'; + return (
    -
    +
    {children}
    From 43fef58196d9e1af0dfbcb10c62bcf59467bad3c Mon Sep 17 00:00:00 2001 From: Madison Date: Sun, 12 Oct 2025 23:13:56 -0500 Subject: [PATCH 33/55] start codeblock-refs --- docs/code-examples/README.md | 74 ++ docs/code-examples/index.ts | 22 + docs/code-examples/setup.ts | 652 ++++++++++++++++++ .../docs/(guides)/getting-started/setup.mdx | 645 +---------------- docs/lib/code-examples.ts | 32 + .../src/components/mdx/platform-codeblock.tsx | 265 ++++--- 6 files changed, 967 insertions(+), 723 deletions(-) create mode 100644 docs/code-examples/README.md create mode 100644 docs/code-examples/index.ts create mode 100644 docs/code-examples/setup.ts create mode 100644 docs/lib/code-examples.ts diff --git a/docs/code-examples/README.md b/docs/code-examples/README.md new file mode 100644 index 0000000000..6fd40a29e1 --- /dev/null +++ b/docs/code-examples/README.md @@ -0,0 +1,74 @@ +# Code Examples + +TypeScript-based code examples for Stack Auth documentation. + +## Structure + +``` +code-examples/ +├── getting-started.ts # All examples for getting-started/* pages +├── index.ts # Aggregates all examples +└── README.md +``` + +## TypeScript Format + +Each TypeScript file exports examples for a documentation section: + +```typescript +import { CodeExample } from '../lib/code-examples'; + +export const gettingStartedExamples = { + 'setup': { + 'example-name': [ + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'server', // optional: "server" or "client" + code: `import { StackServerApp } from "@stackframe/stack"; + +export const stackServerApp = new StackServerApp({ + tokenStore: "nextjs-cookie", +});`, + highlightLanguage: 'typescript', + filename: 'stack/server.ts' + } + ] as CodeExample[] + } +}; +``` + +## Fields + +- **language**: Programming language (e.g., "JavaScript", "Python") +- **framework**: Framework/runtime (e.g., "Next.js", "React", "Django") +- **variant**: (Optional) "server" or "client" for frameworks with both +- **code**: The actual code (use template literals for multi-line!) +- **highlightLanguage**: Syntax highlighting (e.g., "typescript", "python", "bash") +- **filename**: Display filename in docs + +## Usage in MDX + +```jsx + +``` + +## Benefits of TypeScript + +- ✅ **Native template literals** - no escaping needed! +- ✅ **Full IDE support** - syntax highlighting, auto-complete +- ✅ **Type safety** - catch errors at build time +- ✅ **Auto-formatting** - Prettier formats the code for you +- ✅ **Cleaner diffs** - changes are easy to review + +## Tips + +- Use template literals for multi-line code +- Format code as you would in a real file +- Indentation is preserved exactly as written +- Group related examples under the same document subsection +- Add new sections by creating new TypeScript files and importing them in `index.ts` diff --git a/docs/code-examples/index.ts b/docs/code-examples/index.ts new file mode 100644 index 0000000000..eb5e651722 --- /dev/null +++ b/docs/code-examples/index.ts @@ -0,0 +1,22 @@ +import { CodeExample } from '../lib/code-examples'; +import { gettingStartedExamples } from './setup'; + +const allExamples: Record>> = { + 'getting-started': gettingStartedExamples, + // Add more sections here as needed: + // 'auth': authExamples, + // 'customization': customizationExamples, +}; + +export function getExample(documentPath: string, exampleName: string): CodeExample[] | undefined { + const [section, ...rest] = documentPath.split('/'); + const subsection = rest.join('/'); + return allExamples[section]?.[subsection]?.[exampleName]; +} + +export function getDocumentExamples(documentPath: string): Record | undefined { + const [section, ...rest] = documentPath.split('/'); + const subsection = rest.join('/'); + return allExamples[section]?.[subsection]; +} + diff --git a/docs/code-examples/setup.ts b/docs/code-examples/setup.ts new file mode 100644 index 0000000000..765c03fcbb --- /dev/null +++ b/docs/code-examples/setup.ts @@ -0,0 +1,652 @@ +import { CodeExample } from '../lib/code-examples'; + +export const setupExamples = { + 'setup': { + 'env-wizard': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `NEXT_PUBLIC_STACK_PROJECT_ID= +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + highlightLanguage: 'bash', + filename: '.env.local' + }, + { + language: 'JavaScript', + framework: 'React', + code: `// Update the values in stack/client.ts created by the wizard +export const stackClientApp = new StackClientApp({ + projectId: "your-project-id", + publishableClientKey: "your-publishable-client-key", + tokenStore: "cookie", +});`, + highlightLanguage: 'typescript', + filename: 'stack/client.ts' + }, + { + language: 'JavaScript', + framework: 'JavaScript', + code: `STACK_PROJECT_ID= +STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + highlightLanguage: 'bash', + filename: '.env' + } + ] as CodeExample[], + + 'install-package': [ + { language: 'JavaScript', framework: 'Next.js', code: 'npm install @stackframe/stack', highlightLanguage: 'bash', filename: 'Terminal' }, + { language: 'JavaScript', framework: 'React', code: 'npm install @stackframe/react', highlightLanguage: 'bash', filename: 'Terminal' }, + { language: 'JavaScript', framework: 'Express', code: 'npm install @stackframe/js', highlightLanguage: 'bash', filename: 'Terminal' }, + { language: 'JavaScript', framework: 'Node.js', code: 'npm install @stackframe/js', highlightLanguage: 'bash', filename: 'Terminal' }, + { language: 'Python', framework: 'Django', code: 'pip install requests', highlightLanguage: 'bash', filename: 'Terminal' }, + { language: 'Python', framework: 'FastAPI', code: 'pip install requests', highlightLanguage: 'bash', filename: 'Terminal' }, + { language: 'Python', framework: 'Flask', code: 'pip install requests', highlightLanguage: 'bash', filename: 'Terminal' }, + ] as CodeExample[], + + 'env-config': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `NEXT_PUBLIC_STACK_PROJECT_ID= +NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + highlightLanguage: 'env', + filename: '.env.local' + }, + { + language: 'JavaScript', + framework: 'React', + code: `# Store these in environment variables or directly in the client file during development +VITE_STACK_PROJECT_ID= +VITE_STACK_PUBLISHABLE_CLIENT_KEY=`, + highlightLanguage: 'env', + filename: '.env' + }, + { + language: 'JavaScript', + framework: 'Express', + code: `STACK_PROJECT_ID= +STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + highlightLanguage: 'env', + filename: '.env' + }, + { + language: 'JavaScript', + framework: 'Node.js', + code: `STACK_PROJECT_ID= +STACK_PUBLISHABLE_CLIENT_KEY= +STACK_SECRET_SERVER_KEY=`, + highlightLanguage: 'env', + filename: '.env' + }, + { + language: 'Python', + framework: 'Django', + code: `import os + +stack_project_id = os.getenv("STACK_PROJECT_ID") +stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") +stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, + highlightLanguage: 'python', + filename: 'settings.py' + }, + { + language: 'Python', + framework: 'FastAPI', + code: `import os + +stack_project_id = os.getenv("STACK_PROJECT_ID") +stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") +stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, + highlightLanguage: 'python', + filename: 'main.py' + }, + { + language: 'Python', + framework: 'Flask', + code: `import os + +stack_project_id = os.getenv("STACK_PROJECT_ID") +stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") +stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, + highlightLanguage: 'python', + filename: 'app.py' + } + ] as CodeExample[], + + 'stack-config': [ + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'server', + code: `import "server-only"; +import { StackServerApp } from "@stackframe/stack"; + +export const stackServerApp = new StackServerApp({ + tokenStore: "nextjs-cookie", // storing auth tokens in cookies +});`, + highlightLanguage: 'typescript', + filename: 'stack/server.ts' + }, + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'client', + code: `import { StackClientApp } from "@stackframe/stack"; + +export const stackClientApp = new StackClientApp({ + // Environment variables are automatically read +});`, + highlightLanguage: 'typescript', + filename: 'stack/client.ts' + }, + { + language: 'JavaScript', + framework: 'React', + code: `import { StackClientApp } from "@stackframe/react"; +// If you use React Router, uncomment the next line and the redirectMethod below +// import { useNavigate } from "react-router-dom"; + +export const stackClientApp = new StackClientApp({ + projectId: process.env.VITE_STACK_PROJECT_ID || "your-project-id", + publishableClientKey: process.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY || "your-publishable-client-key", + tokenStore: "cookie", + // redirectMethod: { useNavigate }, // Optional: only if using react-router-dom +});`, + highlightLanguage: 'typescript', + filename: 'stack/client.ts' + }, + { + language: 'JavaScript', + framework: 'Express', + variant: 'server', + code: `import { StackServerApp } from "@stackframe/js"; + +export const stackServerApp = new StackServerApp({ + projectId: process.env.STACK_PROJECT_ID, + publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY, + secretServerKey: process.env.STACK_SECRET_SERVER_KEY, + tokenStore: "memory", +});`, + highlightLanguage: 'typescript', + filename: 'stack/server.ts' + }, + { + language: 'JavaScript', + framework: 'Express', + variant: 'client', + code: `import { StackClientApp } from "@stackframe/js"; + +export const stackClientApp = new StackClientApp({ + projectId: process.env.STACK_PROJECT_ID, + publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY, + tokenStore: "cookie", +});`, + highlightLanguage: 'typescript', + filename: 'stack/client.ts' + }, + { + language: 'JavaScript', + framework: 'Node.js', + variant: 'server', + code: `import { StackServerApp } from "@stackframe/js"; + +export const stackServerApp = new StackServerApp({ + projectId: process.env.STACK_PROJECT_ID, + publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY, + secretServerKey: process.env.STACK_SECRET_SERVER_KEY, + tokenStore: "memory", +});`, + highlightLanguage: 'javascript', + filename: 'stack/server.js' + }, + { + language: 'JavaScript', + framework: 'Node.js', + variant: 'client', + code: `import { StackClientApp } from "@stackframe/js"; + +export const stackClientApp = new StackClientApp({ + projectId: process.env.STACK_PROJECT_ID, + publishableClientKey: process.env.STACK_PUBLISHABLE_CLIENT_KEY, + tokenStore: "cookie", +});`, + highlightLanguage: 'javascript', + filename: 'stack/client.js' + }, + { + language: 'Python', + framework: 'Django', + code: `import requests + +def stack_auth_request(method, endpoint, **kwargs): + res = requests.request( + method, + f'https://api.stack-auth.com/{endpoint}', + headers={ + 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API + 'x-stack-project-id': stack_project_id, + 'x-stack-publishable-client-key': stack_publishable_client_key, + 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' + **kwargs.pop('headers', {}), + }, + **kwargs, + ) + if res.status_code >= 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json()`, + highlightLanguage: 'python', + filename: 'views.py' + }, + { + language: 'Python', + framework: 'FastAPI', + code: `import requests + +def stack_auth_request(method, endpoint, **kwargs): + res = requests.request( + method, + f'https://api.stack-auth.com/{endpoint}', + headers={ + 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API + 'x-stack-project-id': stack_project_id, + 'x-stack-publishable-client-key': stack_publishable_client_key, + 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' + **kwargs.pop('headers', {}), + }, + **kwargs, + ) + if res.status_code >= 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json()`, + highlightLanguage: 'python', + filename: 'main.py' + }, + { + language: 'Python', + framework: 'Flask', + code: `import requests + +def stack_auth_request(method, endpoint, **kwargs): + res = requests.request( + method, + f'https://api.stack-auth.com/{endpoint}', + headers={ + 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API + 'x-stack-project-id': stack_project_id, + 'x-stack-publishable-client-key': stack_publishable_client_key, + 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' + **kwargs.pop('headers', {}), + }, + **kwargs, + ) + if res.status_code >= 400: + raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") + return res.json()`, + highlightLanguage: 'python', + filename: 'app.py' + } + ] as CodeExample[], + + 'auth-handlers': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `import { StackHandler } from "@stackframe/stack"; +import { stackServerApp } from "@/stack/server"; + +export default function Handler(props: unknown) { + return ; +}`, + highlightLanguage: 'typescript', + filename: 'app/handler/[...stack]/page.tsx' + }, + { + language: 'JavaScript', + framework: 'React', + code: `import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; +import { Suspense } from "react"; +import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; +import { stackClientApp } from "./stack/client"; + +function HandlerRoutes() { + const location = useLocation(); + return ( + + ); +} + +export default function App() { + return ( + + + + + + } /> + hello world
    } /> + + + + + + ); +}`, + highlightLanguage: 'typescript', + filename: 'App.tsx' + }, + { + language: 'JavaScript', + framework: 'Express', + code: `// Express doesn't use built-in handlers +// Use the REST API or integrate with your frontend`, + highlightLanguage: 'typescript', + filename: 'Note' + }, + { + language: 'JavaScript', + framework: 'Node.js', + code: `// Node.js doesn't use built-in handlers +// Use the REST API or integrate with your frontend`, + highlightLanguage: 'javascript', + filename: 'Note' + } + ] as CodeExample[], + + 'app-providers': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `import React from "react"; +import { StackProvider, StackTheme } from "@stackframe/stack"; +import { stackServerApp } from "@/stack/server"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + {children} + + + + + ); +}`, + highlightLanguage: 'typescript', + filename: 'app/layout.tsx' + }, + { + language: 'JavaScript', + framework: 'React', + code: `// Already shown in the App.tsx example above +// Make sure to wrap your app with StackProvider and StackTheme`, + highlightLanguage: 'typescript', + filename: 'Note' + } + ] as CodeExample[], + + 'loading-boundary': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `export default function Loading() { + // You can use any loading indicator here + return <> + Loading... + ; +}`, + highlightLanguage: 'typescript', + filename: 'app/loading.tsx' + } + ] as CodeExample[], + + 'test-setup': [ + { + language: 'JavaScript', + framework: 'Next.js', + code: `# Start your Next.js app +npm run dev + +# Navigate to the sign-up page +# http://localhost:3000/handler/sign-up`, + highlightLanguage: 'bash', + filename: 'Terminal' + }, + { + language: 'JavaScript', + framework: 'React', + code: `# Start your React app +npm run dev + +# Navigate to the sign-up page +# http://localhost:5173/handler/sign-up`, + highlightLanguage: 'bash', + filename: 'Terminal' + }, + { + language: 'JavaScript', + framework: 'Express', + code: `# Start your Express server +npm start + +# Use the REST API or integrate with your frontend +# Check the REST API documentation for endpoints`, + highlightLanguage: 'bash', + filename: 'Terminal' + }, + { + language: 'JavaScript', + framework: 'Node.js', + code: `# Start your Node.js app +node index.js + +# Use the REST API or integrate with your frontend +# Check the REST API documentation for endpoints`, + highlightLanguage: 'bash', + filename: 'Terminal' + }, + { + language: 'Python', + framework: 'Django', + code: `# Test the Stack Auth API connection +print(stack_auth_request('GET', '/api/v1/projects/current')) + +# Start your Django server +python manage.py runserver`, + highlightLanguage: 'python', + filename: 'Terminal' + }, + { + language: 'Python', + framework: 'FastAPI', + code: `# Test the Stack Auth API connection +print(stack_auth_request('GET', '/api/v1/projects/current')) + +# Start your FastAPI server +uvicorn main:app --reload`, + highlightLanguage: 'python', + filename: 'Terminal' + }, + { + language: 'Python', + framework: 'Flask', + code: `# Test the Stack Auth API connection +print(stack_auth_request('GET', '/api/v1/projects/current')) + +# Start your Flask server +flask run`, + highlightLanguage: 'python', + filename: 'Terminal' + } + ] as CodeExample[], + + 'basic-usage': [ + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'server', + code: `import { stackServerApp } from "@/stack/server"; + +// In a Server Component or API route +const user = await stackServerApp.getUser(); +if (user) { + console.log("User is signed in:", user.displayName); +} else { + console.log("User is not signed in"); +}`, + highlightLanguage: 'typescript', + filename: 'Server Component' + }, + { + language: 'JavaScript', + framework: 'Next.js', + variant: 'client', + code: `'use client'; +import { useUser } from "@stackframe/stack"; + +export default function MyComponent() { + const user = useUser(); + + if (user) { + return
    Hello, {user.displayName}!
    ; + } else { + return
    Please sign in
    ; + } +}`, + highlightLanguage: 'typescript', + filename: 'Client Component' + }, + { + language: 'JavaScript', + framework: 'React', + code: `import { useUser } from "@stackframe/react"; + +export default function MyComponent() { + const user = useUser(); + + if (user) { + return
    Hello, {user.displayName}!
    ; + } else { + return
    Please sign in
    ; + } +}`, + highlightLanguage: 'typescript', + filename: 'Component' + }, + { + language: 'JavaScript', + framework: 'Express', + code: `import { stackServerApp } from "./stack/server.js"; + +app.get('/profile', async (req, res) => { + try { + // Get access token from request headers + const accessToken = req.headers['x-stack-access-token']; + const user = await stackServerApp.getUser({ accessToken }); + + if (user) { + res.json({ message: \`Hello, \${user.displayName}!\` }); + } else { + res.status(401).json({ error: 'Not authenticated' }); + } + } catch (error) { + res.status(500).json({ error: 'Server error' }); + } +});`, + highlightLanguage: 'typescript', + filename: 'server.ts' + }, + { + language: 'JavaScript', + framework: 'Node.js', + code: `import { stackServerApp } from "./stack/server.js"; + +async function checkUser(accessToken) { + try { + const user = await stackServerApp.getUser({ accessToken }); + + if (user) { + console.log(\`Hello, \${user.displayName}!\`); + } else { + console.log('User not authenticated'); + } + } catch (error) { + console.error('Error:', error); + } +}`, + highlightLanguage: 'javascript', + filename: 'index.js' + }, + { + language: 'Python', + framework: 'Django', + code: `# In your views.py +def profile_view(request): + # Get access token from request headers + access_token = request.headers.get('X-Stack-Access-Token') + + try: + user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ + 'x-stack-access-token': access_token, + }) + return JsonResponse({'message': f"Hello, {user_data['displayName']}!"}) + except Exception as e: + return JsonResponse({'error': 'Not authenticated'}, status=401)`, + highlightLanguage: 'python', + filename: 'views.py' + }, + { + language: 'Python', + framework: 'FastAPI', + code: `from fastapi import FastAPI, Header, HTTPException + +app = FastAPI() + +@app.get("/profile") +async def get_profile(x_stack_access_token: str = Header(None)): + if not x_stack_access_token: + raise HTTPException(status_code=401, detail="Access token required") + + try: + user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ + 'x-stack-access-token': x_stack_access_token, + }) + return {"message": f"Hello, {user_data['displayName']}!"} + except Exception as e: + raise HTTPException(status_code=401, detail="Not authenticated")`, + highlightLanguage: 'python', + filename: 'main.py' + }, + { + language: 'Python', + framework: 'Flask', + code: `from flask import Flask, request, jsonify + +app = Flask(__name__) + +@app.route('/profile') +def profile(): + access_token = request.headers.get('X-Stack-Access-Token') + + if not access_token: + return jsonify({'error': 'Access token required'}), 401 + + try: + user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ + 'x-stack-access-token': access_token, + }) + return jsonify({'message': f"Hello, {user_data['displayName']}!"}) + except Exception as e: + return jsonify({'error': 'Not authenticated'}), 401`, + highlightLanguage: 'python', + filename: 'app.py' + } + ] as CodeExample[] + } +}; + diff --git a/docs/content/docs/(guides)/getting-started/setup.mdx b/docs/content/docs/(guides)/getting-started/setup.mdx index 153a34b67e..3e4fb8340c 100644 --- a/docs/content/docs/(guides)/getting-started/setup.mdx +++ b/docs/content/docs/(guides)/getting-started/setup.mdx @@ -50,36 +50,8 @@ We recommend using our **setup wizard** for JavaScript frameworks for a seamless Create an account on [the Stack Auth dashboard](https://app.stack-auth.com/projects), create a new project with an API key, and copy its environment variables into the appropriate configuration file: -NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= -STACK_SECRET_SERVER_KEY=`, - language: "bash", - filename: ".env.local" - }, - "React": { - code: `// Update the values in stack/client.ts created by the wizard -export const stackClientApp = new StackClientApp({ - projectId: "your-project-id", - publishableClientKey: "your-publishable-client-key", - tokenStore: "cookie", -});`, - language: "typescript", - filename: "stack/client.ts" - }, - "JavaScript": { - code: `STACK_PROJECT_ID= -STACK_PUBLISHABLE_CLIENT_KEY= -STACK_SECRET_SERVER_KEY=`, - language: "bash", - filename: ".env" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js" }} + document="setup/setup" + examples={["env-wizard"]} title="Environment Configuration" /> @@ -117,49 +89,8 @@ STACK_SECRET_SERVER_KEY=`, First, install the appropriate Stack package: @@ -176,69 +107,8 @@ STACK_SECRET_SERVER_KEY=`, Set up your environment variables or configuration: -NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= -STACK_SECRET_SERVER_KEY=`, - language: "env", - filename: ".env.local" - }, - "React": { - code: `# Store these in environment variables or directly in the client file during development -VITE_STACK_PROJECT_ID= -VITE_STACK_PUBLISHABLE_CLIENT_KEY=`, - language: "env", - filename: ".env" - }, - "Express": { - code: `STACK_PROJECT_ID= -STACK_PUBLISHABLE_CLIENT_KEY= -STACK_SECRET_SERVER_KEY=`, - language: "env", - filename: ".env" - }, - "Node.js": { - code: `STACK_PROJECT_ID= -STACK_PUBLISHABLE_CLIENT_KEY= -STACK_SECRET_SERVER_KEY=`, - language: "env", - filename: ".env" - } - }, - "Python": { - "Django": { - code: `import os - -stack_project_id = os.getenv("STACK_PROJECT_ID") -stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") -stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, - language: "python", - filename: "settings.py" - }, - "FastAPI": { - code: `import os - -stack_project_id = os.getenv("STACK_PROJECT_ID") -stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") -stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, - language: "python", - filename: "main.py" - }, - "Flask": { - code: `import os - -stack_project_id = os.getenv("STACK_PROJECT_ID") -stack_publishable_client_key = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY") -stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, - language: "python", - filename: "app.py" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js", "Python": "Django" }} + document="setup/setup" + examples={["env-config"]} title="Environment Configuration" /> @@ -249,165 +119,8 @@ stack_secret_server_key = os.getenv("STACK_SECRET_SERVER_KEY")`, Create the Stack app configuration: = 400: - raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") - return res.json()`, - language: "python", - filename: "views.py" - }, - "FastAPI": { - code: `import requests - -def stack_auth_request(method, endpoint, **kwargs): - res = requests.request( - method, - f'https://api.stack-auth.com/{endpoint}', - headers={ - 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' - **kwargs.pop('headers', {}), - }, - **kwargs, - ) - if res.status_code >= 400: - raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") - return res.json()`, - language: "python", - filename: "main.py" - }, - "Flask": { - code: `import requests - -def stack_auth_request(method, endpoint, **kwargs): - res = requests.request( - method, - f'https://api.stack-auth.com/{endpoint}', - headers={ - 'x-stack-access-type': 'server', # or 'client' if you're only accessing the client API - 'x-stack-project-id': stack_project_id, - 'x-stack-publishable-client-key': stack_publishable_client_key, - 'x-stack-secret-server-key': stack_secret_server_key, # not necessary if access type is 'client' - **kwargs.pop('headers', {}), - }, - **kwargs, - ) - if res.status_code >= 400: - raise Exception(f"Stack Auth API request failed with {res.status_code}: {res.text}") - return res.json()`, - language: "python", - filename: "app.py" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js", "Python": "Django" }} + document="setup/setup" + examples={["stack-config"]} title="Stack Configuration" /> @@ -418,66 +131,8 @@ def stack_auth_request(method, endpoint, **kwargs): For JavaScript frameworks, create the authentication handler: ; -}`, - language: "typescript", - filename: "app/handler/[...stack]/page.tsx" - }, - "React": { - code: `import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; - import { Suspense } from "react"; - import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; - import { stackClientApp } from "./stack/client"; - - function HandlerRoutes() { - const location = useLocation(); - return ( - - ); - } - - export default function App() { - return ( - - - - - - } /> - hello world
    } /> - - - - - - ); -}`, - language: "typescript", - filename: "App.tsx" - }, - "Express": { - code: `// Express doesn't use built-in handlers -// Use the REST API or integrate with your frontend`, - language: "typescript", - filename: "Note" - }, - "Node.js": { - code: `// Node.js doesn't use built-in handlers -// Use the REST API or integrate with your frontend`, - language: "javascript", - filename: "Note" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js" }} + document="setup/setup" + examples={["auth-handlers"]} title="Authentication Handlers" /> @@ -488,39 +143,8 @@ def stack_auth_request(method, endpoint, **kwargs): For Next.js and React, wrap your app with Stack providers: - - - - {children} - - - - - ); -}`, - language: "typescript", - filename: "app/layout.tsx" - }, - "React": { - code: `// Already shown in the App.tsx example above -// Make sure to wrap your app with StackProvider and StackTheme`, - language: "typescript", - filename: "Note" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js" }} + document="setup/setup" + examples={["app-providers"]} title="App Providers" /> @@ -531,22 +155,8 @@ export default function RootLayout({ children }: { children: React.ReactNode }) For Next.js, add a Suspense boundary: - Loading... - ; -}`, - language: "typescript", - filename: "app/loading.tsx" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js" }} + document="setup/setup" + examples={["loading-boundary"]} title="Loading Component" /> @@ -564,77 +174,8 @@ That's it! Stack is now configured in your project. ### Testing your setup @@ -657,158 +198,8 @@ For Python and backend-only JavaScript setups, you'll interact with Stack Auth t Here are some basic usage examples for each platform: Hello, {user.displayName}!
    ; - } else { - return
    Please sign in
    ; - } -}`, - language: "typescript", - filename: "Client Component" - } - }, - "React": { - code: `import { useUser } from "@stackframe/react"; - -export default function MyComponent() { - const user = useUser(); - - if (user) { - return
    Hello, {user.displayName}!
    ; - } else { - return
    Please sign in
    ; - } -}`, - language: "typescript", - filename: "Component" - }, - "Express": { - code: `import { stackServerApp } from "./stack/server.js"; - -app.get('/profile', async (req, res) => { - try { - // Get access token from request headers - const accessToken = req.headers['x-stack-access-token']; - const user = await stackServerApp.getUser({ accessToken }); - - if (user) { - res.json({ message: \`Hello, \${user.displayName}!\` }); - } else { - res.status(401).json({ error: 'Not authenticated' }); - } - } catch (error) { - res.status(500).json({ error: 'Server error' }); - } -});`, - language: "typescript", - filename: "server.ts" - }, - "Node.js": { - code: `import { stackServerApp } from "./stack/server.js"; - -async function checkUser(accessToken) { - try { - const user = await stackServerApp.getUser({ accessToken }); - - if (user) { - console.log(\`Hello, \${user.displayName}!\`); - } else { - console.log('User not authenticated'); - } - } catch (error) { - console.error('Error:', error); - } -}`, - language: "javascript", - filename: "index.js" - } - }, - "Python": { - "Django": { - code: `# In your views.py -def profile_view(request): - # Get access token from request headers - access_token = request.headers.get('X-Stack-Access-Token') - - try: - user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ - 'x-stack-access-token': access_token, - }) - return JsonResponse({'message': f"Hello, {user_data['displayName']}!"}) - except Exception as e: - return JsonResponse({'error': 'Not authenticated'}, status=401)`, - language: "python", - filename: "views.py" - }, - "FastAPI": { - code: `from fastapi import FastAPI, Header, HTTPException - -app = FastAPI() - -@app.get("/profile") -async def get_profile(x_stack_access_token: str = Header(None)): - if not x_stack_access_token: - raise HTTPException(status_code=401, detail="Access token required") - - try: - user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ - 'x-stack-access-token': x_stack_access_token, - }) - return {"message": f"Hello, {user_data['displayName']}!"} - except Exception as e: - raise HTTPException(status_code=401, detail="Not authenticated")`, - language: "python", - filename: "main.py" - }, - "Flask": { - code: `from flask import Flask, request, jsonify - -app = Flask(__name__) - -@app.route('/profile') -def profile(): - access_token = request.headers.get('X-Stack-Access-Token') - - if not access_token: - return jsonify({'error': 'Access token required'}), 401 - - try: - user_data = stack_auth_request('GET', '/api/v1/users/me', headers={ - 'x-stack-access-token': access_token, - }) - return jsonify({'message': f"Hello, {user_data['displayName']}!"}) - except Exception as e: - return jsonify({'error': 'Not authenticated'}), 401`, - language: "python", - filename: "app.py" - } - } - }} - defaultPlatform="JavaScript" - defaultFrameworks={{ "JavaScript": "Next.js", "Python": "Django" }} + document="getting-started/setup" + examples={["basic-usage"]} title="Basic Usage Examples" /> diff --git a/docs/lib/code-examples.ts b/docs/lib/code-examples.ts new file mode 100644 index 0000000000..fbc2a3e7f1 --- /dev/null +++ b/docs/lib/code-examples.ts @@ -0,0 +1,32 @@ +/** + * Centralized code examples for Stack Auth documentation + * + * Examples are stored as TypeScript files in: docs/code-examples/ + * + * Structure: + * - language: Programming language (e.g., "JavaScript", "Python") + * - framework: Framework (e.g., "Next.js", "React", "Django") + * - variant: Optional "server" or "client" + * - code: The actual code (use template literals for multi-line) + * - highlightLanguage: Syntax highlighting language + * - filename: Display filename + */ + +export type CodeExample = { + language: string; + framework: string; + variant?: 'server' | 'client'; + code: string; + highlightLanguage: string; + filename?: string; +}; + +export type CodeExamplesMap = { + [documentPath: string]: { + [exampleName: string]: CodeExample[]; + }; +}; + +// Re-export functions from the code-examples index +export { getDocumentExamples, getExample } from '../code-examples'; + diff --git a/docs/src/components/mdx/platform-codeblock.tsx b/docs/src/components/mdx/platform-codeblock.tsx index 632cf8e453..a85d1e9a92 100644 --- a/docs/src/components/mdx/platform-codeblock.tsx +++ b/docs/src/components/mdx/platform-codeblock.tsx @@ -2,8 +2,9 @@ import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; import { ChevronDown } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { codeToHtml } from 'shiki'; +import { getExample, type CodeExample } from '../../../lib/code-examples'; import { cn } from '../../lib/cn'; // Global state management for platform and framework selection @@ -72,10 +73,28 @@ function broadcastFrameworkChange(platform: string, framework: string): void { export type PlatformCodeblockProps = { /** - * Platform configurations with their frameworks and code examples - * Can include server/client variants for each framework + * Document path in the code-examples.ts file (e.g., "getting-started/setup") */ - platforms: { + document: string, + /** + * Array of example names to include from the document + */ + examples: string[], + /** + * Optional title for the code block + */ + title?: string, + /** + * Additional CSS classes + */ + className?: string, +} + +/** + * Converts CodeExample[] from code-examples.ts to the platforms format + */ +function convertExamplesToPlatforms(examples: CodeExample[]) { + const platforms: { [platformName: string]: { [frameworkName: string]: { code: string, @@ -88,43 +107,99 @@ export type PlatformCodeblockProps = { filename?: string, }, client: { - code: string, + code: string, language?: string, filename?: string, }, }, }, - }, - /** - * Default platform to show - */ - defaultPlatform?: string, - /** - * Default framework to show for each platform - */ - defaultFrameworks?: { [platformName: string]: string }, - /** - * Default server/client selection for each platform/framework - */ - defaultVariants?: VariantSelections, - /** - * Optional title for the code block - */ - title?: string, - /** - * Additional CSS classes - */ - className?: string, + } = {}; + + const defaultFrameworks: { [platformName: string]: string } = {}; + const defaultVariants: VariantSelections = {}; + + for (const example of examples) { + const { language, framework, variant, code, filename, highlightLanguage } = example; + + // Initialize language if not exists + if (!(language in platforms)) { + platforms[language] = {}; + } + + // Set as default framework if first for this language + if (!(language in defaultFrameworks)) { + defaultFrameworks[language] = framework; + } + + if (variant) { + // Has server/client variant - initialize if not already a variant config + // We check if 'server' exists to determine if it's already been initialized as a variant config + if (!('server' in (platforms[language][framework] ?? {}))) { + platforms[language][framework] = { + server: { code: '', language: highlightLanguage }, + client: { code: '', language: highlightLanguage } + }; + } + + const variantConfig = platforms[language][framework] as { + server: { code: string, language?: string, filename?: string }, + client: { code: string, language?: string, filename?: string }, + }; + + // Explicitly narrow the variant type + const variantType: 'server' | 'client' = variant; + variantConfig[variantType] = { + code, + language: highlightLanguage, + filename + }; + + // Initialize default variants + if (!(language in defaultVariants)) { + defaultVariants[language] = {}; + } + if (!defaultVariants[language]?.[framework]) { + defaultVariants[language]![framework] = 'server'; + } + } else { + // No variant + platforms[language][framework] = { + code, + language: highlightLanguage, + filename + }; + } + } + + // Determine default platform (first one in the list) + const defaultPlatform = Object.keys(platforms)[0]; + + return { platforms, defaultPlatform, defaultFrameworks, defaultVariants }; } export function PlatformCodeblock({ - platforms, - defaultPlatform, - defaultFrameworks = {}, - defaultVariants = {}, + document: documentPath, + examples: exampleNames, title, className }: PlatformCodeblockProps) { + // Load and convert examples from the centralized code-examples.ts file + const allExamples: CodeExample[] = []; + + for (const exampleName of exampleNames) { + const examples = getExample(documentPath, exampleName); + if (!examples) { + console.warn(`Example "${exampleName}" not found in document "${documentPath}"`); + continue; + } + allExamples.push(...examples); + } + + // Convert to the internal platforms format + const { platforms, defaultPlatform, defaultFrameworks, defaultVariants } = allExamples.length > 0 + ? convertExamplesToPlatforms(allExamples) + : { platforms: {}, defaultPlatform: '', defaultFrameworks: {}, defaultVariants: {} }; + const platformNames = Object.keys(platforms); const firstPlatform = defaultPlatform || platformNames[0]; @@ -180,7 +255,17 @@ export function PlatformCodeblock({ const [highlightedCode, setHighlightedCode] = useState(''); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [dropdownView, setDropdownView] = useState<'platform' | 'framework'>('platform'); - const [componentId] = useState(() => Math.random().toString(36).substr(2, 9)); + // Generate stable ID based on props to avoid hydration mismatches + const componentId = useMemo(() => { + const hashString = `${documentPath}-${exampleNames.join(',')}`; + let hash = 0; + for (let i = 0; i < hashString.length; i++) { + const char = hashString.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(36).substr(0, 9); + }, [documentPath, exampleNames]); // Get current framework options for selected platform const currentFrameworks = Object.keys(platforms[selectedPlatform] ?? {}); @@ -357,7 +442,10 @@ export function PlatformCodeblock({ const handleDropdownToggle = () => { setIsDropdownOpen(!isDropdownOpen); - setDropdownView('platform'); + // Don't reset dropdownView when just opening/closing + if (!isDropdownOpen) { + setDropdownView('platform'); + } }; const handleVariantChange = (variant: 'server' | 'client') => { @@ -379,82 +467,64 @@ export function PlatformCodeblock({
    {title && (
    -
    {title}
    -
    - )} - - {/* Server/Client Tabs and Cascading Dropdown */} -
    - {/* Left Side: Server/Client Tabs or Current Selection */} -
    - {hasVariants(selectedPlatform, currentFramework) ? ( - // Show Server/Client tabs when variants exist -
    - {(['server', 'client'] as const).map((variant) => ( +
    +
    {title}
    +
    + {/* File Title in Title Section */} + {currentCodeConfig?.filename && ( +
    + {currentCodeConfig.filename} +
    + )} + {/* Dropdown Button with Current Selection */} +
    - ))} -
    - ) : ( - // Show current selection when no variants -
    - {selectedPlatform} - / - {currentFramework} +
    - )} -
    - - {/* Right Side: Cascading Dropdown */} -
    - -
    -
    - - {/* File Title Bar */} - {currentCodeConfig?.filename && ( -
    -
    - {currentCodeConfig.filename}
    )} {/* Code Content */} -
    +
    + {/* Server/Client Tabs (if variants exist) */} + {hasVariants(selectedPlatform, currentFramework) && ( +
    + {(['server', 'client'] as const).map((variant) => ( + + ))} +
    + )} +
    {dropdownView === 'platform' ? ( From 39e85e58944a824f2e514fc7ee5b49edd5618882 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 13 Oct 2025 00:48:52 -0500 Subject: [PATCH 34/55] fixing links --- .../(guides)/getting-started/components.mdx | 4 +- .../platform-codeblock-example.mdx | 613 ------------------ .../(guides)/getting-started/production.mdx | 2 +- docs/content/docs/(guides)/meta.json | 2 +- 4 files changed, 4 insertions(+), 617 deletions(-) delete mode 100644 docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx diff --git a/docs/content/docs/(guides)/getting-started/components.mdx b/docs/content/docs/(guides)/getting-started/components.mdx index 8fdd3c3c36..a12bea4ede 100644 --- a/docs/content/docs/(guides)/getting-started/components.mdx +++ b/docs/content/docs/(guides)/getting-started/components.mdx @@ -5,7 +5,7 @@ description: Pre-built Next.js components to make your life easier In [the last guide](./setup.mdx), we initialized Stack. This time, we will take a quick look at some of the most useful Next.js components. -For the full documentation of all available components, please refer to the [components reference](/components). +For the full documentation of all available components, please refer to the [components reference](../components). ## `` @@ -55,7 +55,7 @@ To change the default sign-in URL to your own, see the documentation on [custom ## Others -Stack has many more components available. For a comprehensive list, please check the documentation on [components](../components/overview). +Stack has many more components available. For a comprehensive list, please check the documentation on [components](../components). ## Next steps diff --git a/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx b/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx deleted file mode 100644 index 49228d2017..0000000000 --- a/docs/content/docs/(guides)/getting-started/platform-codeblock-example.mdx +++ /dev/null @@ -1,613 +0,0 @@ ---- -title: "Platform Codeblock Example" ---- - -# Platform Codeblock Example - -Here's how to use the new PlatformCodeblock component: - - bool: - # Your user validation logic here - return True # Placeholder - -def create_jwt_token(username: str) -> str: - # Your JWT token creation logic here - return "your-jwt-token" # Placeholder`, - language: "python" - }, - "Flask": { - code: `from flask import Flask, request, jsonify -from werkzeug.security import check_password_hash -from flask_jwt_extended import JWTManager, create_access_token - -app = Flask(__name__) -app.config['JWT_SECRET_KEY'] = 'your-secret-key' -jwt = JWTManager(app) - -@app.route('/login', methods=['POST']) -def login(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - - # Validate user credentials (implement your logic) - user = get_user_by_username(username) - if user and check_password_hash(user.password_hash, password): - access_token = create_access_token(identity=username) - return jsonify({ - 'access_token': access_token, - 'success': True - }) - - return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 - -def get_user_by_username(username): - # Your user lookup logic here - pass # Placeholder`, - language: "python" - } - }, - "JavaScript": { - "Next.js": { - code: `import { NextRequest, NextResponse } from 'next/server'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; - -export async function POST(request: NextRequest) { - try { - const { username, password } = await request.json(); - - // Validate user credentials (implement your logic) - const user = await getUserByUsername(username); - if (!user) { - return NextResponse.json( - { success: false, error: 'Invalid credentials' }, - { status: 401 } - ); - } - - const isValidPassword = await bcrypt.compare(password, user.passwordHash); - if (!isValidPassword) { - return NextResponse.json( - { success: false, error: 'Invalid credentials' }, - { status: 401 } - ); - } - - // Create JWT token - const token = jwt.sign( - { userId: user.id, username: user.username }, - process.env.JWT_SECRET!, - { expiresIn: '24h' } - ); - - return NextResponse.json({ - success: true, - token, - user: { id: user.id, username: user.username } - }); - } catch (error) { - return NextResponse.json( - { success: false, error: 'Internal server error' }, - { status: 500 } - ); - } -} - -async function getUserByUsername(username: string) { - // Your user lookup logic here - return null; // Placeholder -}`, - language: "typescript" - }, - "Express": { - code: `const express = require('express'); -const bcrypt = require('bcryptjs'); -const jwt = require('jsonwebtoken'); -const app = express(); - -app.use(express.json()); - -app.post('/login', async (req, res) => { - try { - const { username, password } = req.body; - - // Validate user credentials (implement your logic) - const user = await getUserByUsername(username); - if (!user) { - return res.status(401).json({ - success: false, - error: 'Invalid credentials' - }); - } - - const isValidPassword = await bcrypt.compare(password, user.passwordHash); - if (!isValidPassword) { - return res.status(401).json({ - success: false, - error: 'Invalid credentials' - }); - } - - // Create JWT token - const token = jwt.sign( - { userId: user.id, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: '24h' } - ); - - res.json({ - success: true, - token, - user: { id: user.id, username: user.username } - }); - } catch (error) { - res.status(500).json({ - success: false, - error: 'Internal server error' - }); - } -}); - -async function getUserByUsername(username) { - // Your user lookup logic here - return null; // Placeholder -} - -app.listen(3000, () => { - console.log('Server running on port 3000'); -});`, - language: "javascript" - }, - "React": { - code: `import React, { useState } from 'react'; -import axios from 'axios'; - -interface LoginFormProps { - onLoginSuccess: (token: string, user: any) => void; -} - -export function LoginForm({ onLoginSuccess }: LoginFormProps) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(''); - - try { - const response = await axios.post('/api/login', { - username, - password - }); - - if (response.data.success) { - onLoginSuccess(response.data.token, response.data.user); - } else { - setError(response.data.error || 'Login failed'); - } - } catch (err: any) { - setError(err.response?.data?.error || 'Network error'); - } finally { - setLoading(false); - } - }; - - return ( -
    -
    - - setUsername(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" - required - /> -
    - -
    - - setPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" - required - /> -
    - - {error && ( -
    {error}
    - )} - - -
    - ); -}`, - language: "typescript" - } - } - }} -/> - - bool: - # Your user validation logic here - return True # Placeholder - -def create_jwt_token(username: str) -> str: - # Your JWT token creation logic here - return "your-jwt-token" # Placeholder`, - language: "python" - }, - "Flask": { - code: `from flask import Flask, request, jsonify -from werkzeug.security import check_password_hash -from flask_jwt_extended import JWTManager, create_access_token - -app = Flask(__name__) -app.config['JWT_SECRET_KEY'] = 'your-secret-key' -jwt = JWTManager(app) - -@app.route('/login', methods=['POST']) -def login(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - - # Validate user credentials (implement your logic) - user = get_user_by_username(username) - if user and check_password_hash(user.password_hash, password): - access_token = create_access_token(identity=username) - return jsonify({ - 'access_token': access_token, - 'success': True - }) - - return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 - -def get_user_by_username(username): - # Your user lookup logic here - pass # Placeholder`, - language: "python" - } - }, - "JavaScript": { - "Next.js": { - code: `import { NextRequest, NextResponse } from 'next/server'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; - -export async function POST(request: NextRequest) { - try { - const { username, password } = await request.json(); - - // Validate user credentials (implement your logic) - const user = await getUserByUsername(username); - if (!user) { - return NextResponse.json( - { success: false, error: 'Invalid credentials' }, - { status: 401 } - ); - } - - const isValidPassword = await bcrypt.compare(password, user.passwordHash); - if (!isValidPassword) { - return NextResponse.json( - { success: false, error: 'Invalid credentials' }, - { status: 401 } - ); - } - - // Create JWT token - const token = jwt.sign( - { userId: user.id, username: user.username }, - process.env.JWT_SECRET!, - { expiresIn: '24h' } - ); - - return NextResponse.json({ - success: true, - token, - user: { id: user.id, username: user.username } - }); - } catch (error) { - return NextResponse.json( - { success: false, error: 'Internal server error' }, - { status: 500 } - ); - } -} - -async function getUserByUsername(username: string) { - // Your user lookup logic here - return null; // Placeholder -}`, - language: "typescript" - }, - "Express": { - code: `const express = require('express'); -const bcrypt = require('bcryptjs'); -const jwt = require('jsonwebtoken'); -const app = express(); - -app.use(express.json()); - -app.post('/login', async (req, res) => { - try { - const { username, password } = req.body; - - // Validate user credentials (implement your logic) - const user = await getUserByUsername(username); - if (!user) { - return res.status(401).json({ - success: false, - error: 'Invalid credentials' - }); - } - - const isValidPassword = await bcrypt.compare(password, user.passwordHash); - if (!isValidPassword) { - return res.status(401).json({ - success: false, - error: 'Invalid credentials' - }); - } - - // Create JWT token - const token = jwt.sign( - { userId: user.id, username: user.username }, - process.env.JWT_SECRET, - { expiresIn: '24h' } - ); - - res.json({ - success: true, - token, - user: { id: user.id, username: user.username } - }); - } catch (error) { - res.status(500).json({ - success: false, - error: 'Internal server error' - }); - } -}); - -async function getUserByUsername(username) { - // Your user lookup logic here - return null; // Placeholder -} - -app.listen(3000, () => { - console.log('Server running on port 3000'); -});`, - language: "javascript" - }, - "React": { - code: `import React, { useState } from 'react'; -import axios from 'axios'; - -interface LoginFormProps { - onLoginSuccess: (token: string, user: any) => void; -} - -export function LoginForm({ onLoginSuccess }: LoginFormProps) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(''); - - try { - const response = await axios.post('/api/login', { - username, - password - }); - - if (response.data.success) { - onLoginSuccess(response.data.token, response.data.user); - } else { - setError(response.data.error || 'Login failed'); - } - } catch (err: any) { - setError(err.response?.data?.error || 'Network error'); - } finally { - setLoading(false); - } - }; - - return ( -
    -
    - - setUsername(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" - required - /> -
    - -
    - - setPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" - required - /> -
    - - {error && ( -
    {error}
    - )} - - -
    - ); -}`, - language: "typescript" - } - } - }} -/> - -## Usage - -The PlatformCodeblock component accepts the following props: - -- `platforms`: An object defining the platforms and their frameworks with code examples -- `defaultPlatform`: The platform to show by default -- `defaultFrameworks`: Default framework for each platform -- `title`: Optional title for the code block -- `className`: Additional CSS classes - -The component will automatically: -- Show a platform selector on the left side -- Show framework tabs on the right side for the selected platform -- Syntax highlight the code based on the specified language -- Adapt to light/dark theme changes -- Persist selections as users switch between platforms and frameworks diff --git a/docs/content/docs/(guides)/getting-started/production.mdx b/docs/content/docs/(guides)/getting-started/production.mdx index 2f1365f2e1..fef31d7176 100644 --- a/docs/content/docs/(guides)/getting-started/production.mdx +++ b/docs/content/docs/(guides)/getting-started/production.mdx @@ -15,7 +15,7 @@ Follow these steps when you're ready to push your application to production: ## Add Your Domain - 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/stack-app.mdx)). + 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/stack-app)). ## Disable Localhost Callbacks diff --git a/docs/content/docs/(guides)/meta.json b/docs/content/docs/(guides)/meta.json index 53086f03ef..cc516b7168 100644 --- a/docs/content/docs/(guides)/meta.json +++ b/docs/content/docs/(guides)/meta.json @@ -10,7 +10,7 @@ "getting-started/components", "getting-started/users", "getting-started/production", - "getting-started/platform-codeblock-example", + "getting-started/example-pages", "---Concepts---", "concepts/api-keys", "concepts/backend-integration", From d4b77746accfd9fb48a603c855a913228d9d8600 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 13 Oct 2025 00:49:05 -0500 Subject: [PATCH 35/55] finally fix styling --- docs/src/app/api/layout.tsx | 5 +++-- docs/src/app/docs/layout.tsx | 5 +++-- .../components/layouts/api-layout-wrapper.tsx | 20 +++++++++++++++++++ .../layouts/docs-layout-wrapper.tsx | 20 +++++++++++++++++++ docs/src/components/layouts/docs.tsx | 2 +- 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 docs/src/components/layouts/api-layout-wrapper.tsx create mode 100644 docs/src/components/layouts/docs-layout-wrapper.tsx diff --git a/docs/src/app/api/layout.tsx b/docs/src/app/api/layout.tsx index 316c71e6e9..40e35315d9 100644 --- a/docs/src/app/api/layout.tsx +++ b/docs/src/app/api/layout.tsx @@ -1,6 +1,7 @@ import { APIPageWrapper } from '@/components/api/api-page-wrapper'; import { AuthPanel } from '@/components/api/auth-panel'; import { AIChatDrawer } from '@/components/chat/ai-chat'; +import { ApiLayoutWrapper } from '@/components/layouts/api-layout-wrapper'; import { ApiSidebar } from '@/components/layouts/api/api-sidebar-server'; import { DocsHeaderWrapper } from '@/components/layouts/docs-header-wrapper'; import { SharedContentLayout } from '@/components/layouts/shared-content-layout'; @@ -91,7 +92,7 @@ export default function ApiLayout({ children }: { children: React.ReactNode }) { return ( -
    +
    {/* Full-width header with Stack Auth branding */} -
    +
    ); diff --git a/docs/src/app/docs/layout.tsx b/docs/src/app/docs/layout.tsx index 96af3e42b8..c4f384e5dc 100644 --- a/docs/src/app/docs/layout.tsx +++ b/docs/src/app/docs/layout.tsx @@ -1,5 +1,6 @@ import { DocsHeaderWrapper } from '@/components/layouts/docs-header-wrapper'; import { DynamicDocsLayout } from '@/components/layouts/docs-layout-router'; +import { DocsLayoutWrapper } from '@/components/layouts/docs-layout-wrapper'; import { SidebarProvider } from '@/components/layouts/sidebar-context'; import { source } from 'lib/source'; import type { ReactNode } from 'react'; @@ -8,7 +9,7 @@ import './custom-docs-styles.css'; export default function DocsLayout({ children }: { children: ReactNode }) { return ( -
    + {/* Docs Header Wrapper - Provides sidebar content to mobile navigation */}
    -
    +
    ); } diff --git a/docs/src/components/layouts/api-layout-wrapper.tsx b/docs/src/components/layouts/api-layout-wrapper.tsx new file mode 100644 index 0000000000..f66897fe36 --- /dev/null +++ b/docs/src/components/layouts/api-layout-wrapper.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { cn } from '@/lib/cn'; +import { type ReactNode } from 'react'; +import { useSidebar } from './sidebar-context'; + +export function ApiLayoutWrapper({ children }: { children: ReactNode }) { + const sidebarContext = useSidebar(); + const isCollapsed = sidebarContext?.isMainSidebarCollapsed ?? false; + + return ( +
    + {children} +
    + ); +} + diff --git a/docs/src/components/layouts/docs-layout-wrapper.tsx b/docs/src/components/layouts/docs-layout-wrapper.tsx new file mode 100644 index 0000000000..bfa378013e --- /dev/null +++ b/docs/src/components/layouts/docs-layout-wrapper.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { cn } from '@/lib/cn'; +import { type ReactNode } from 'react'; +import { useSidebar } from './sidebar-context'; + +export function DocsLayoutWrapper({ children }: { children: ReactNode }) { + const sidebarContext = useSidebar(); + const isCollapsed = sidebarContext?.isMainSidebarCollapsed ?? false; + + return ( +
    + {children} +
    + ); +} + diff --git a/docs/src/components/layouts/docs.tsx b/docs/src/components/layouts/docs.tsx index b29eec8603..a0ecd2fd6f 100644 --- a/docs/src/components/layouts/docs.tsx +++ b/docs/src/components/layouts/docs.tsx @@ -838,6 +838,7 @@ export function DocsLayout({ const pageStyles: PageStyles = { tocNav: cn('xl:hidden'), toc: cn('max-xl:hidden'), + article: cn('max-w-none'), }; return ( @@ -1083,4 +1084,3 @@ function CodeOverlayRenderer() { export { getSidebarTabsFromOptions } from './docs/shared'; export { CollapsibleControl, Navbar, NavbarSidebarTrigger, type LinkItemType }; - From 54c7ae62c68d8a1fb3a3fbd7b82158d95307d37a Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 13 Oct 2025 01:22:50 -0500 Subject: [PATCH 36/55] remove dumb readme --- docs/code-examples/README.md | 74 ------------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 docs/code-examples/README.md diff --git a/docs/code-examples/README.md b/docs/code-examples/README.md deleted file mode 100644 index 6fd40a29e1..0000000000 --- a/docs/code-examples/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Code Examples - -TypeScript-based code examples for Stack Auth documentation. - -## Structure - -``` -code-examples/ -├── getting-started.ts # All examples for getting-started/* pages -├── index.ts # Aggregates all examples -└── README.md -``` - -## TypeScript Format - -Each TypeScript file exports examples for a documentation section: - -```typescript -import { CodeExample } from '../lib/code-examples'; - -export const gettingStartedExamples = { - 'setup': { - 'example-name': [ - { - language: 'JavaScript', - framework: 'Next.js', - variant: 'server', // optional: "server" or "client" - code: `import { StackServerApp } from "@stackframe/stack"; - -export const stackServerApp = new StackServerApp({ - tokenStore: "nextjs-cookie", -});`, - highlightLanguage: 'typescript', - filename: 'stack/server.ts' - } - ] as CodeExample[] - } -}; -``` - -## Fields - -- **language**: Programming language (e.g., "JavaScript", "Python") -- **framework**: Framework/runtime (e.g., "Next.js", "React", "Django") -- **variant**: (Optional) "server" or "client" for frameworks with both -- **code**: The actual code (use template literals for multi-line!) -- **highlightLanguage**: Syntax highlighting (e.g., "typescript", "python", "bash") -- **filename**: Display filename in docs - -## Usage in MDX - -```jsx - -``` - -## Benefits of TypeScript - -- ✅ **Native template literals** - no escaping needed! -- ✅ **Full IDE support** - syntax highlighting, auto-complete -- ✅ **Type safety** - catch errors at build time -- ✅ **Auto-formatting** - Prettier formats the code for you -- ✅ **Cleaner diffs** - changes are easy to review - -## Tips - -- Use template literals for multi-line code -- Format code as you would in a real file -- Indentation is preserved exactly as written -- Group related examples under the same document subsection -- Add new sections by creating new TypeScript files and importing them in `index.ts` From bf0dd013ade7e4e03c72a1eca69c5a10763ef072 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 13 Oct 2025 02:46:21 -0500 Subject: [PATCH 37/55] update code examples, and styling on codeblock --- docs/code-examples/index.ts | 4 +- docs/code-examples/setup.ts | 8 +- .../docs/(guides)/getting-started/setup.mdx | 2 +- .../src/components/mdx/platform-codeblock.tsx | 285 +++++++++--------- 4 files changed, 152 insertions(+), 147 deletions(-) diff --git a/docs/code-examples/index.ts b/docs/code-examples/index.ts index eb5e651722..675121e4ac 100644 --- a/docs/code-examples/index.ts +++ b/docs/code-examples/index.ts @@ -1,8 +1,8 @@ import { CodeExample } from '../lib/code-examples'; -import { gettingStartedExamples } from './setup'; +import { setupExamples } from './setup'; const allExamples: Record>> = { - 'getting-started': gettingStartedExamples, + 'setup': setupExamples, // Add more sections here as needed: // 'auth': authExamples, // 'customization': customizationExamples, diff --git a/docs/code-examples/setup.ts b/docs/code-examples/setup.ts index 765c03fcbb..76c52f2e73 100644 --- a/docs/code-examples/setup.ts +++ b/docs/code-examples/setup.ts @@ -52,7 +52,7 @@ STACK_SECRET_SERVER_KEY=`, code: `NEXT_PUBLIC_STACK_PROJECT_ID= NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY= STACK_SECRET_SERVER_KEY=`, - highlightLanguage: 'env', + highlightLanguage: 'bash', filename: '.env.local' }, { @@ -61,7 +61,7 @@ STACK_SECRET_SERVER_KEY=`, code: `# Store these in environment variables or directly in the client file during development VITE_STACK_PROJECT_ID= VITE_STACK_PUBLISHABLE_CLIENT_KEY=`, - highlightLanguage: 'env', + highlightLanguage: 'bash', filename: '.env' }, { @@ -70,7 +70,7 @@ VITE_STACK_PUBLISHABLE_CLIENT_KEY=`, code: `STACK_PROJECT_ID= STACK_PUBLISHABLE_CLIENT_KEY= STACK_SECRET_SERVER_KEY=`, - highlightLanguage: 'env', + highlightLanguage: 'bash', filename: '.env' }, { @@ -79,7 +79,7 @@ STACK_SECRET_SERVER_KEY=`, code: `STACK_PROJECT_ID= STACK_PUBLISHABLE_CLIENT_KEY= STACK_SECRET_SERVER_KEY=`, - highlightLanguage: 'env', + highlightLanguage: 'bash', filename: '.env' }, { diff --git a/docs/content/docs/(guides)/getting-started/setup.mdx b/docs/content/docs/(guides)/getting-started/setup.mdx index 3e4fb8340c..313e30bcc6 100644 --- a/docs/content/docs/(guides)/getting-started/setup.mdx +++ b/docs/content/docs/(guides)/getting-started/setup.mdx @@ -198,7 +198,7 @@ For Python and backend-only JavaScript setups, you'll interact with Stack Auth t Here are some basic usage examples for each platform: diff --git a/docs/src/components/mdx/platform-codeblock.tsx b/docs/src/components/mdx/platform-codeblock.tsx index a85d1e9a92..4ebe1622d9 100644 --- a/docs/src/components/mdx/platform-codeblock.tsx +++ b/docs/src/components/mdx/platform-codeblock.tsx @@ -205,12 +205,7 @@ export function PlatformCodeblock({ // Initialize with global platform or default const getInitialPlatform = () => { - if (typeof window !== 'undefined') { - const stored = sessionStorage.getItem('stack-docs-selected-platform'); - if (stored && platformNames.includes(stored)) { - return stored; - } - } + // Use consistent default during SSR and initial render return globalSelectedPlatform && platformNames.includes(globalSelectedPlatform) ? globalSelectedPlatform : firstPlatform; @@ -218,6 +213,7 @@ export function PlatformCodeblock({ // Initialize global frameworks with defaults if not already set const initializeGlobalFrameworks = () => { + // Load from sessionStorage if available (only on client) if (typeof window !== 'undefined') { const stored = sessionStorage.getItem('stack-docs-selected-frameworks'); if (stored) { @@ -230,11 +226,16 @@ export function PlatformCodeblock({ } } - // Set defaults for any platforms that don't have a framework selected + // Only set defaults for platforms that have stored selections or if explicitly needed + // Don't auto-select frameworks during initialization platformNames.forEach(platform => { if (!globalSelectedFrameworks[platform]) { const frameworks = Object.keys(platforms[platform]); - globalSelectedFrameworks[platform] = defaultFrameworks[platform] || frameworks[0]; + if (frameworks.length > 0) { + // Set first framework as default but don't broadcast the change + // This allows manual selection without auto-selection + globalSelectedFrameworks[platform] = defaultFrameworks[platform] || frameworks[0]; + } } }); }; @@ -246,6 +247,7 @@ export function PlatformCodeblock({ const [selectedPlatform, setSelectedPlatform] = useState(getInitialPlatform); const [selectedFrameworks, setSelectedFrameworks] = useState<{ [platform: string]: string }>(() => { + // Don't initialize with defaults - let users manually select frameworks return { ...globalSelectedFrameworks }; }); const [selectedVariants, setSelectedVariants] = useState(() => { @@ -423,15 +425,7 @@ export function PlatformCodeblock({ const handlePlatformSelect = (platform: string) => { broadcastPlatformChange(platform); - // Show framework selection for this platform setDropdownView('framework'); - - // Auto-select first framework of new platform - const newPlatformFrameworks = Object.keys(platforms[platform] ?? {}); - if (newPlatformFrameworks.length > 0) { - const firstFramework = defaultFrameworks[platform] || newPlatformFrameworks[0]; - broadcastFrameworkChange(platform, firstFramework); - } }; const handleFrameworkSelect = (framework: string) => { @@ -440,19 +434,21 @@ export function PlatformCodeblock({ setDropdownView('platform'); }; - const handleDropdownToggle = () => { - setIsDropdownOpen(!isDropdownOpen); - // Don't reset dropdownView when just opening/closing - if (!isDropdownOpen) { - setDropdownView('platform'); - } + const toggleDropdown = () => { + setIsDropdownOpen(prev => { + const next = !prev; + if (next) { + setDropdownView('platform'); + } + return next; + }); }; const handleVariantChange = (variant: 'server' | 'client') => { setSelectedVariants(prev => ({ ...prev, [selectedPlatform]: { - ...prev[selectedPlatform], + ...(prev[selectedPlatform] ?? {}), [currentFramework]: variant } })); @@ -462,66 +458,141 @@ export function PlatformCodeblock({ return
    No platforms configured
    ; } + const showMetadata = Boolean(title || currentCodeConfig?.filename); + return ( -
    -
    - {title && ( -
    -
    -
    {title}
    -
    - {/* File Title in Title Section */} - {currentCodeConfig?.filename && ( -
    - {currentCodeConfig.filename} +
    +
    +
    + {showMetadata && ( +
    + {title && ( +
    + {title} +
    + )} + {currentCodeConfig?.filename && ( +
    + {currentCodeConfig.filename} +
    + )} +
    + )} + +
    + + + {isDropdownOpen && ( +
    + {dropdownView === 'platform' ? ( +
    +
    + Choose Platform +
    + {platformNames.map((platform) => ( + + ))} +
    + ) : ( +
    +
    + + Select {selectedPlatform} framework +
    + {currentFrameworks.map((framework) => ( + + ))}
    )} - {/* Dropdown Button with Current Selection */} -
    - -
    -
    + )}
    - )} +
    {/* Code Content */} -
    +
    {/* Server/Client Tabs (if variants exist) */} {hasVariants(selectedPlatform, currentFramework) && ( -
    - {(['server', 'client'] as const).map((variant) => ( - - ))} +
    +
    + {(['server', 'client'] as const).map((variant) => ( + + ))} +
    )} @@ -533,72 +604,6 @@ export function PlatformCodeblock({
    - - {/* Single Cascading Dropdown Menu */} - {isDropdownOpen && ( -
    - {dropdownView === 'platform' ? ( - // Platform Selection View -
    -
    - Select Platform -
    - {platformNames.map((platform) => ( - - ))} -
    - ) : ( - // Framework Selection View -
    -
    - - Select {selectedPlatform} Framework -
    - {currentFrameworks.map((framework) => ( - - ))} -
    - )} -
    - )}
    ); } From 54c8993b33bdd54d2e3136983f016d00a542cc09 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 13 Oct 2025 20:25:26 -0500 Subject: [PATCH 38/55] ToC updates --- docs/src/components/layouts/shared-header.tsx | 18 +++++++++++++++--- docs/src/components/mdx/platform-codeblock.tsx | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/src/components/layouts/shared-header.tsx b/docs/src/components/layouts/shared-header.tsx index 4d7e736993..934404dbe8 100644 --- a/docs/src/components/layouts/shared-header.tsx +++ b/docs/src/components/layouts/shared-header.tsx @@ -115,7 +115,7 @@ function TOCToggleButtonInner(props: { className: string }) { return (
    - {/* Content */} -
    -
    +
    +
    {props.children}
    @@ -123,7 +110,7 @@ export function TocItemsEmpty() { const { text } = useI18n(); return ( -
    +
    {text.tocNoHeadings}
    ); @@ -150,20 +137,42 @@ export function TOCScrollArea(props: ComponentProps<'div'>) { export function TOCItems({ items }: { items: TOCItemType[] }) { const containerRef = useRef(null); + const [hiddenTabUrls, setHiddenTabUrls] = useState>(() => new Set()); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const hiddenUrls = new Set(); + + for (const item of items) { + const targetId = extractHash(item.url); + if (!targetId) continue; + + const targetElement = document.getElementById(targetId); + if (!targetElement) continue; + + if (isInsideTabs(targetElement)) { + hiddenUrls.add(item.url); + } + } + + setHiddenTabUrls(hiddenUrls); + }, [items]); + + const visibleItems = useMemo( + () => items.filter((item) => !hiddenTabUrls.has(item.url)), + [items, hiddenTabUrls], + ); - if (items.length === 0) return ; + if (visibleItems.length === 0) return ; return ( <> -
    - {items.map((item) => ( + {visibleItems.map((item) => ( ))}
    @@ -172,11 +181,38 @@ export function TOCItems({ items }: { items: TOCItemType[] }) { } function TOCItem({ item }: { item: TOCItemType }) { + const handleClick = (event: React.MouseEvent) => { + if (typeof window === 'undefined') return; + + const targetId = extractHash(item.url); + if (!targetId) return; + + const initialTarget = document.getElementById(targetId); + if (!initialTarget) return; + + event.preventDefault(); + ensureTabsVisible(initialTarget).then(() => { + requestAnimationFrame(() => { + const visibleTarget = document.getElementById(targetId); + if (!visibleTarget) return; + visibleTarget.scrollIntoView({ behavior: 'smooth', block: 'start' }); + try { + window.history.replaceState(null, '', `#${targetId}`); + } catch { + // no-op if history manipulation is not allowed + } + }); + }); + }; + return ( = 4 && 'ps-8', @@ -186,3 +222,76 @@ function TOCItem({ item }: { item: TOCItemType }) { ); } + +function extractHash(url: string): string | null { + const hashIndex = url.lastIndexOf('#'); + if (hashIndex === -1) return null; + const hash = url.slice(hashIndex + 1); + return hash.length > 0 ? decodeURIComponent(hash) : null; +} + +async function ensureTabsVisible(element: HTMLElement | null): Promise { + if (!element) return; + + const tabChain: HTMLElement[] = []; + let current = element.closest('[data-tabs-content]'); + while (current) { + tabChain.push(current); + current = current.parentElement?.closest('[data-tabs-content]') ?? null; + } + + for (let i = tabChain.length - 1; i >= 0; i--) { + await activateTabContent(tabChain[i]); + } +} + +async function activateTabContent(tabContent: HTMLElement): Promise { + if (tabContent.getAttribute('data-state') === 'active') { + return; + } + + const tabValue = tabContent.getAttribute('data-tab-value'); + if (!tabValue) return; + + const tabsRoot = tabContent.closest('[data-tabs-root]'); + if (!tabsRoot) return; + + const trigger = findTabTrigger(tabsRoot, tabValue); + if (!trigger) return; + + trigger.click(); + await waitFor(() => tabContent.getAttribute('data-state') === 'active'); +} + +function waitFor(condition: () => boolean, timeout = 250): Promise { + return new Promise((resolve) => { + const start = performance.now(); + + const check = () => { + if (condition() || performance.now() - start > timeout) { + resolve(); + return; + } + + requestAnimationFrame(check); + }; + + check(); + }); +} + +function isInsideTabs(element: HTMLElement): boolean { + return Boolean(element.closest('[data-tabs-content]')); +} + +function findTabTrigger(tabsRoot: HTMLElement, tabValue: string): HTMLElement | null { + const triggers = tabsRoot.querySelectorAll('[data-tabs-trigger]'); + + for (const trigger of triggers) { + if (trigger.getAttribute('data-tab-value') === tabValue) { + return trigger; + } + } + + return null; +} diff --git a/docs/src/components/ui/tabs.tsx b/docs/src/components/ui/tabs.tsx index 3c8261cf1a..8dabe8cae8 100644 --- a/docs/src/components/ui/tabs.tsx +++ b/docs/src/components/ui/tabs.tsx @@ -3,17 +3,30 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"; import * as React from "react"; import { cn } from "../../lib/cn"; +type TabsRootProps = React.ComponentPropsWithoutRef & { + "data-tabs-root-id"?: string; +}; + const Tabs = React.forwardRef< React.ComponentRef, - React.ComponentPropsWithoutRef ->((props, ref) => { + TabsRootProps +>(({ className, "data-tabs-root-id": dataTabsRootId, ...props }, ref) => { + const generatedId = React.useId(); + const normalizedId = React.useMemo( + () => `tabs-root-${generatedId.replace(/:/g, "")}`, + [generatedId], + ); + const rootId = dataTabsRootId ?? normalizedId; + return ( ); @@ -27,6 +40,7 @@ const TabsList = React.forwardRef< >((props, ref) => ( , React.ComponentPropsWithoutRef ->((props, ref) => ( +>(({ className, value, ...props }, ref) => ( )); @@ -56,19 +73,23 @@ TabsTrigger.displayName = "TabsTrigger"; const TabsContent = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef ->((props, ref) => ( +>(({ className, value, forceMount = true, ...props }, ref) => ( )); TabsContent.displayName = "TabsContent"; export { Tabs, TabsContent, TabsList, TabsTrigger }; - From b2335d4fbe23e6f15a88d3e6ae91cf5a96a2cc13 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 03:26:30 -0500 Subject: [PATCH 40/55] Update images --- .../docs/(guides)/concepts/team-selection.mdx | 8 +- .../(guides)/getting-started/components.mdx | 21 +++-- .../docs/(guides)/getting-started/setup.mdx | 17 +++- docs/content/docs/(guides)/overview.mdx | 19 ++++- docs/package.json | 1 + docs/src/components/image-zoom.css | 77 +++++++++++++++++++ docs/src/mdx-components.tsx | 5 +- docs/src/src/components/image-zoom.tsx | 58 ++++++++++++++ 8 files changed, 190 insertions(+), 16 deletions(-) create mode 100644 docs/src/components/image-zoom.css create mode 100644 docs/src/src/components/image-zoom.tsx diff --git a/docs/content/docs/(guides)/concepts/team-selection.mdx b/docs/content/docs/(guides)/concepts/team-selection.mdx index 8a7bc760fd..3246a01c14 100644 --- a/docs/content/docs/(guides)/concepts/team-selection.mdx +++ b/docs/content/docs/(guides)/concepts/team-selection.mdx @@ -17,7 +17,13 @@ While the current team method can be simpler to implement, it has a downside. If To facilitate team selection, Stack provides a component that looks like this: -![TeamSwitcher](/imgs/team-switcher.png) +TeamSwitcher You can import and use the `SelectedTeamSwitcher` component for the "current team" method. It updates the `selectedTeam` when a user selects a team: diff --git a/docs/content/docs/(guides)/getting-started/components.mdx b/docs/content/docs/(guides)/getting-started/components.mdx index a12bea4ede..b89104c920 100644 --- a/docs/content/docs/(guides)/getting-started/components.mdx +++ b/docs/content/docs/(guides)/getting-started/components.mdx @@ -11,9 +11,13 @@ For the full documentation of all available components, please refer to the [com The `` component shows the user's avatar that opens a dropdown with various user settings on click. -
    - UserButton -
    +UserButton ```tsx title="page.tsx" import { UserButton } from '@stackframe/stack'; @@ -29,9 +33,14 @@ export default function Page() { These components show a sign-in and sign-up form, respectively. -
    - SignIn -
    + SignIn + ```tsx title="page.tsx" import { SignIn } from '@stackframe/stack'; diff --git a/docs/content/docs/(guides)/getting-started/setup.mdx b/docs/content/docs/(guides)/getting-started/setup.mdx index 313e30bcc6..9468dfba65 100644 --- a/docs/content/docs/(guides)/getting-started/setup.mdx +++ b/docs/content/docs/(guides)/getting-started/setup.mdx @@ -21,6 +21,7 @@ Before getting started, make sure you have a project set up for your chosen plat We recommend using our **setup wizard** for JavaScript frameworks for a seamless installation experience. For Python, we recommend using the REST API approach. +### Setup Wizard / Manual Installation Setup wizard (recommended for JS) @@ -183,13 +184,21 @@ That's it! Stack is now configured in your project. For JavaScript frameworks with built-in UI components, you'll see the Stack Auth sign-up page: -
    - SignIn -
    +Stack sign-in page After signing up/in, you will be redirected back to the home page. You can also check out the account settings page. -![Stack account settings page](/imgs/account-settings.png) +Stack account settings page For Python and backend-only JavaScript setups, you'll interact with Stack Auth through the REST API. diff --git a/docs/content/docs/(guides)/overview.mdx b/docs/content/docs/(guides)/overview.mdx index d06ea0f95d..08e8186d31 100644 --- a/docs/content/docs/(guides)/overview.mdx +++ b/docs/content/docs/(guides)/overview.mdx @@ -78,9 +78,13 @@ The user data will update in both the frontend and backend automatically. The up You also get pages and components for the authentication flow out-of-the-box. This is the sign-in page you get without writing a single line of code: -
    - SignIn -
    + SignIn Notice, there's no branding on our components. We believe we should grow by building the best product, not by forcing our brand onto your users. This means **we depend on your help to spread the word about Stack**. If you like what you're reading, please take a moment to tell one or two of your friends about us. @@ -101,7 +105,14 @@ export default function CustomOAuthSignIn() { To manage everything efficiently, there is a powerful admin dashboard: -![Stack dashboard](/imgs/dashboard.png) +Stack dashboard + Best of all, Stack Auth is **100% open-source**. This means the client, server, dashboard, and even this documentation you're reading right now. Check out our [GitHub](https://github.com/stack-auth/stack-auth) to open an issue or pull request. diff --git a/docs/package.json b/docs/package.json index 1a91bb0c47..48c6b3913f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -49,6 +49,7 @@ "posthog-node": "^4.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-medium-image-zoom": "^5.4.0", "react-remove-scroll": "^2.7.0", "remark": "^15.0.1", "remark-gfm": "^4.0.1", diff --git a/docs/src/components/image-zoom.css b/docs/src/components/image-zoom.css new file mode 100644 index 0000000000..23e0e96ba7 --- /dev/null +++ b/docs/src/components/image-zoom.css @@ -0,0 +1,77 @@ +[data-rmiz] { + display: block; + position: relative; +} + +[data-rmiz-ghost] { + pointer-events: none; + position: absolute; +} + +[data-rmiz-btn-zoom], +[data-rmiz-btn-unzoom] { + display: none; +} + +[data-rmiz-content='found'] img { + cursor: zoom-in; +} + +[data-rmiz-modal][open] { + width: 100vw /* fallback */; + width: 100dvw; + + height: 100vh /* fallback */; + height: 100dvh; + + background-color: transparent; + max-width: none; + max-height: none; + margin: 0; + padding: 0; + position: fixed; + overflow: hidden; +} + +[data-rmiz-modal]:focus-visible { + outline: none; +} + +[data-rmiz-modal-overlay] { + transition: background-color 0.3s; + position: absolute; + inset: 0; +} + +[data-rmiz-modal-overlay='visible'] { + background-color: var(--color-fd-background); +} + +[data-rmiz-modal-overlay='hidden'] { + background-color: transparent; +} + +[data-rmiz-modal-content] { + width: 100%; + height: 100%; + position: relative; +} + +[data-rmiz-modal]::backdrop { + display: none; +} + +[data-rmiz-modal-img] { + cursor: zoom-out; + image-rendering: high-quality; + transform-origin: 0 0; + transition: transform 0.3s; + position: absolute; +} + +@media (prefers-reduced-motion: reduce) { + [data-rmiz-modal-overlay], + [data-rmiz-modal-img] { + transition-duration: 0.01ms !important; + } +} diff --git a/docs/src/mdx-components.tsx b/docs/src/mdx-components.tsx index dc72f8f3a6..6bf7dba8b0 100644 --- a/docs/src/mdx-components.tsx +++ b/docs/src/mdx-components.tsx @@ -26,6 +26,7 @@ import { Mermaid } from './components/mdx/mermaid'; import { Accordion, AccordionGroup, ClickableTableOfContents, CodeBlocks, Icon, Markdown, ParamField } from './components/mdx/sdk-components'; import { PropTable } from './components/prop-table'; +import { ImageZoom } from 'fumadocs-ui/components/image-zoom'; import { SignInDemo, SignInExtraInfo, SignInPasswordFirstTab, SignInStackAuth } from './components/stack-auth/sign-in'; import { AccountSettingsStackAuth } from './components/stack-auth/stack-account-settings'; import { TeamSwitcherDemo } from './components/stack-auth/stack-team-switcher'; @@ -89,6 +90,8 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { CursorIcon, // UI Components Button, - JWTViewer + JWTViewer, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + img: (props) => , } as MDXComponents; } diff --git a/docs/src/src/components/image-zoom.tsx b/docs/src/src/components/image-zoom.tsx new file mode 100644 index 0000000000..2a711c582b --- /dev/null +++ b/docs/src/src/components/image-zoom.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { Image, type ImageProps } from 'fumadocs-core/framework'; +import { type ImgHTMLAttributes } from 'react'; +import Zoom, { type UncontrolledProps } from 'react-medium-image-zoom'; +import '../../components/image-zoom.css'; + +export type ImageZoomProps = ImageProps & { + /** + * Image props when zoom in + */ + zoomInProps?: ImgHTMLAttributes, + + /** + * Props for `react-medium-image-zoom` + */ + rmiz?: UncontrolledProps, +}; + +function getImageSrc(src: ImageProps['src']): string { + if (typeof src === 'string') return src; + + if (typeof src === 'object') { + // Next.js + if ('default' in src) + return (src as { default: { src: string } }).default.src; + return src.src; + } + + return ''; +} + +export function ImageZoom({ + zoomInProps, + children, + rmiz, + ...props +}: ImageZoomProps) { + return ( + + {children ?? ( + + )} + + ); +} From e92d60b2a9505e6a7bb0ecb979889b353ff0232f Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 03:27:48 -0500 Subject: [PATCH 41/55] update pnpm-lock --- pnpm-lock.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6edb17856b..ed5364296e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -658,6 +658,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-medium-image-zoom: + specifier: ^5.4.0 + version: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-remove-scroll: specifier: ^2.7.0 version: 2.7.1(@types/react@18.3.12)(react@18.3.1) @@ -14174,8 +14177,8 @@ packages: '@types/react': ^18.2.0 react: '>=18' - react-medium-image-zoom@5.2.14: - resolution: {integrity: sha512-nfTVYcAUnBzXQpPDcZL+cG/e6UceYUIG+zDcnemL7jtAqbJjVVkA85RgneGtJeni12dTyiRPZVM6Szkmwd/o8w==} + react-medium-image-zoom@5.4.0: + resolution: {integrity: sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -28056,7 +28059,7 @@ snapshots: postcss-selector-parser: 7.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-medium-image-zoom: 5.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-medium-image-zoom: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-remove-scroll: 2.7.1(@types/react@18.3.12)(react@18.3.1) tailwind-merge: 3.3.1 optionalDependencies: @@ -28088,7 +28091,7 @@ snapshots: postcss-selector-parser: 7.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-medium-image-zoom: 5.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-medium-image-zoom: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-remove-scroll: 2.7.1(@types/react@18.3.12)(react@18.3.1) tailwind-merge: 3.3.1 optionalDependencies: @@ -31797,7 +31800,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-medium-image-zoom@5.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-medium-image-zoom@5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) From 9142d17b8fe523e7fa2d36031b1f733d61d42097 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 03:31:28 -0500 Subject: [PATCH 42/55] update build --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78d3701f3d..cc064b2e19 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:backend": "pnpm pre && turbo run build --filter=@stackframe/stack-backend...", "build:dashboard": "pnpm pre && turbo run build --filter=@stackframe/stack-dashboard...", "build:demo": "pnpm pre && turbo run build --filter=demo-app...", - "build:docs": "pnpm run build:packages-only && pnpm run build:backend && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", + "build:docs": "pnpm run build:packages && pnpm run build:backend && turbo run generate-openapi-fumadocs && pnpm run --filter=@stackframe/stack-docs generate-openapi-docs && turbo run build --filter=@stackframe/stack-docs", "build:packages": "pnpm pre && turbo run build --filter=./packages/*", "claude-code": "pnpm pre && npx -y @anthropic-ai/claude-code@latest", "cmux": "pnpm pre && npx -y cmux@latest", From 73fef2ee665719f67a66734001a0c3144cd6ddfe Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 13:59:10 -0500 Subject: [PATCH 43/55] lint --- docs/src/components/layout/toc.tsx | 2 ++ docs/src/components/layouts/docs/shared.tsx | 21 +++++++++++++++++-- .../src/components/mdx/platform-codeblock.tsx | 8 +++---- docs/src/components/ui/tabs.tsx | 2 +- docs/src/src/components/image-zoom.tsx | 1 + 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/src/components/layout/toc.tsx b/docs/src/components/layout/toc.tsx index cd36787db6..54cb3202be 100644 --- a/docs/src/components/layout/toc.tsx +++ b/docs/src/components/layout/toc.tsx @@ -202,6 +202,8 @@ function TOCItem({ item }: { item: TOCItemType }) { // no-op if history manipulation is not allowed } }); + }).catch(() => { + // Handle promise rejection silently }); }; diff --git a/docs/src/components/layouts/docs/shared.tsx b/docs/src/components/layouts/docs/shared.tsx index 38ebd14bcb..33c790ff33 100644 --- a/docs/src/components/layouts/docs/shared.tsx +++ b/docs/src/components/layouts/docs/shared.tsx @@ -3,8 +3,7 @@ import { getSidebarTabs, type GetSidebarTabsOptions, } from 'fumadocs-ui/utils/get-sidebar-tabs'; -import type { ReactNode } from 'react'; -import type { Option } from '../../layout/root-toggle'; +import type { ComponentProps, ReactNode } from 'react'; import { type SidebarComponents, SidebarFolder, @@ -20,6 +19,24 @@ export const layoutVariables = { '--fd-layout-offset': 'max(calc(50vw - var(--fd-layout-width) / 2), 0px)', }; +// Local copy of the fumadocs RootToggle option type to avoid depending on the removed component. +export type Option = { + /** + * Redirect URL of the folder, usually the index page + */ + url: string, + icon?: ReactNode, + title: ReactNode, + description?: ReactNode, + + /** + * Detect from a list of urls + */ + urls?: Set, + + props?: ComponentProps<'a'>, +}; + export type SidebarOptions = { components?: Partial, diff --git a/docs/src/components/mdx/platform-codeblock.tsx b/docs/src/components/mdx/platform-codeblock.tsx index 1c6217dedc..7930a9f1f9 100644 --- a/docs/src/components/mdx/platform-codeblock.tsx +++ b/docs/src/components/mdx/platform-codeblock.tsx @@ -2,7 +2,7 @@ import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; import { ChevronDown } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { codeToHtml } from 'shiki'; import { getExample, type CodeExample } from '../../../lib/code-examples'; import { cn } from '../../lib/cn'; @@ -212,7 +212,7 @@ export function PlatformCodeblock({ }; // Initialize global frameworks with defaults if not already set - const initializeGlobalFrameworks = () => { + const initializeGlobalFrameworks = useCallback(() => { // Load from sessionStorage if available (only on client) if (typeof window !== 'undefined') { const stored = sessionStorage.getItem('stack-docs-selected-frameworks'); @@ -238,12 +238,12 @@ export function PlatformCodeblock({ } } }); - }; + }, [platformNames, platforms, defaultFrameworks]); // Initialize global state on first render useEffect(() => { initializeGlobalFrameworks(); - }, []); + }, [initializeGlobalFrameworks]); const [selectedPlatform, setSelectedPlatform] = useState(getInitialPlatform); const [selectedFrameworks, setSelectedFrameworks] = useState<{ [platform: string]: string }>(() => { diff --git a/docs/src/components/ui/tabs.tsx b/docs/src/components/ui/tabs.tsx index 8dabe8cae8..de97dd432c 100644 --- a/docs/src/components/ui/tabs.tsx +++ b/docs/src/components/ui/tabs.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { cn } from "../../lib/cn"; type TabsRootProps = React.ComponentPropsWithoutRef & { - "data-tabs-root-id"?: string; + "data-tabs-root-id"?: string, }; const Tabs = React.forwardRef< diff --git a/docs/src/src/components/image-zoom.tsx b/docs/src/src/components/image-zoom.tsx index 2a711c582b..07ab8db5a8 100644 --- a/docs/src/src/components/image-zoom.tsx +++ b/docs/src/src/components/image-zoom.tsx @@ -49,6 +49,7 @@ export function ImageZoom({ > {children ?? ( {props.alt From 32b4c329207384d2c033e3764b43430b27ba1774 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 14:02:33 -0500 Subject: [PATCH 44/55] update sdk docs to prod --- docs/content/docs/sdk/index.mdx | 8 + docs/content/docs/sdk/meta.json | 2 + docs/content/docs/sdk/overview-new.mdx | 51 +++++ docs/content/docs/sdk/types/customer.mdx | 255 +++++++++++++++++++++++ docs/content/docs/sdk/types/item.mdx | 236 +++++++++++++++++++++ docs/content/docs/sdk/types/project.mdx | 2 +- docs/content/docs/sdk/types/team.mdx | 135 +++++++++++- docs/content/docs/sdk/types/user.mdx | 124 +++++++++++ 8 files changed, 809 insertions(+), 4 deletions(-) create mode 100644 docs/content/docs/sdk/overview-new.mdx create mode 100644 docs/content/docs/sdk/types/customer.mdx create mode 100644 docs/content/docs/sdk/types/item.mdx diff --git a/docs/content/docs/sdk/index.mdx b/docs/content/docs/sdk/index.mdx index a85c720d88..eb4e08f43f 100644 --- a/docs/content/docs/sdk/index.mdx +++ b/docs/content/docs/sdk/index.mdx @@ -45,6 +45,14 @@ export const sdkSections = [ { name: "SendEmailOptions", href: "types/email#sendemailoptions", icon: "type" }, ] }, + { + title: "Payments & Items", + items: [ + { name: "Customer", href: "types/customer#customer", icon: "type" }, + { name: "Item", href: "types/item#item", icon: "type" }, + { name: "ServerItem", href: "types/item#serveritem", icon: "type" }, + ] + }, { title: "Hooks", items: [ diff --git a/docs/content/docs/sdk/meta.json b/docs/content/docs/sdk/meta.json index 0e64521f08..1b4ebdc314 100644 --- a/docs/content/docs/sdk/meta.json +++ b/docs/content/docs/sdk/meta.json @@ -18,6 +18,8 @@ "types/api-key", "types/project", "types/connected-account", + "types/item", + "types/customer", "---Hooks---", "hooks/use-stack-app", "hooks/use-user" diff --git a/docs/content/docs/sdk/overview-new.mdx b/docs/content/docs/sdk/overview-new.mdx new file mode 100644 index 0000000000..8609f00b48 --- /dev/null +++ b/docs/content/docs/sdk/overview-new.mdx @@ -0,0 +1,51 @@ +--- +title: SDK Overview +--- + + +This is the SDK reference for Stack Auth's Next.js SDK. + +For a list of components, see the [Components](../components) page. For instructions on how to get started and how to use the SDK, see the [Setup & Installation](../getting-started/setup.mdx) page. If you are using a framework or programming language other than Next.js, you can use [our REST API](../rest-api). + +export const sdkSections = [ + { + title: "General", + items: [ + { name: "StackClientApp", href: "./objects/stack-app#stackclientapp", icon: "object" }, + { name: "StackServerApp", href: "./sdk/objects/stack-app#stackserverapp", icon: "object" }, + { name: "Project", href: "./sdk/types/project#project", icon: "type" }, + ] + }, + { + title: "Users & user data", + items: [ + { name: "CurrentUser", href: "./sdk/types/user#currentuser", icon: "type" }, + { name: "ServerUser", href: "./sdk/types/user#serveruser", icon: "type" }, + { name: "CurrentServerUser", href: "./sdk/types/user#currentserveruser", icon: "type" }, + { name: "ContactChannel", href: "./sdk/types/contact-channel#contactchannel", icon: "type" }, + { name: "ServerContactChannel", href: "./sdk/types/contact-channel#servercontactchannel", icon: "type" }, + ] + }, + { + title: "Teams", + items: [ + { name: "Team", href: "./sdk/types/team#team", icon: "type" }, + { name: "ServerTeam", href: "./sdk/types/team#serverteam", icon: "type" }, + { name: "TeamPermission", href: "./sdk/types/team-permission#teampermission", icon: "type" }, + { name: "ServerTeamPermission", href: "./sdk/types/team-permission#serverteampermission", icon: "type" }, + { name: "TeamUser", href: "./sdk/types/team-user#teamuser", icon: "type" }, + { name: "ServerTeamUser", href: "./sdk/types/team-user#serverteamuser", icon: "type" }, + { name: "TeamProfile", href: "./sdk/types/team-profile#teamprofile", icon: "type" }, + { name: "ServerTeamProfile", href: "./sdk/types/team-profile#serverteamprofile", icon: "type" }, + ] + }, + { + title: "Hooks", + items: [ + { name: "useStackApp", href: "./sdk/hooks/use-stack-app", icon: "hook" }, + { name: "useUser", href: "./sdk/hooks/use-user", icon: "hook" }, + ] + } +]; + + diff --git a/docs/content/docs/sdk/types/customer.mdx b/docs/content/docs/sdk/types/customer.mdx new file mode 100644 index 0000000000..e45ba0e56e --- /dev/null +++ b/docs/content/docs/sdk/types/customer.mdx @@ -0,0 +1,255 @@ +--- +title: Customer +full: true +--- + +The `Customer` interface provides payment and item management functionality that is shared between users and teams. Both [`CurrentUser`](../types/user.mdx#currentuser) and [`Team`](../types/team.mdx#team) types extend this interface, allowing them to create checkout URLs and manage items. + +On this page: +- [Customer](#customer) + +--- + +# `Customer` + +The `Customer` interface defines the payment-related functionality available to both users and teams. It provides methods for creating checkout URLs for purchases and managing quantifiable items like credits, API calls, or subscription allowances. + +This interface is automatically available on: +- [`CurrentUser`](../types/user.mdx#currentuser) objects +- [`Team`](../types/team.mdx#team) objects +- [`ServerUser`](../types/user.mdx#serveruser) objects (with additional server-side capabilities) +- [`ServerTeam`](../types/team.mdx#serverteam) objects (with additional server-side capabilities) + +### Table of Contents + +; //$stack-link-to:#customercreatecheckouturl + getItem(itemId): Promise; //$stack-link-to:#customergetitem + // NEXT_LINE_PLATFORM react-like + ⤷ useItem(itemId): Item; //$stack-link-to:#customeruseitem +};`} /> + + + + + The unique identifier for the customer. For users, this is the user ID; for teams, this is the team ID. + + + + ```typescript + declare const id: string; + ``` + + + + + + + + Creates a secure checkout URL for purchasing a product. This method integrates with Stripe to generate a payment link that handles the entire purchase flow. + + The checkout URL will redirect users to a Stripe-hosted payment page where they can complete their purchase. After successful payment, users will be redirected back to your application. + + ### Parameters + + + Options for creating the checkout URL. + + + The ID of the product to purchase, as configured in your Stack Auth project settings. + + + + + ### Returns + + `Promise`: A secure URL that redirects to the Stripe checkout page for the specified product. + + + + + ```typescript + declare function createCheckoutUrl(options: { + productId: string; + }): Promise; + ``` + + + ```typescript User purchasing a subscription + const user = useUser({ or: "redirect" }); + + const handleUpgrade = async () => { + try { + const checkoutUrl = await user.createCheckoutUrl({ + productId: "prod_premium_monthly", + }); + + // Redirect to Stripe checkout + window.location.href = checkoutUrl; + } catch (error) { + console.error("Failed to create checkout URL:", error); + } + }; + ``` + + ```typescript Team purchasing additional seats + const team = await user.getTeam("team_123"); + + const purchaseSeats = async () => { + const checkoutUrl = await team.createCheckoutUrl({ + productId: "prod_additional_seats", + }); + + // Open checkout in new tab + window.open(checkoutUrl, '_blank'); + }; + ``` + + + + + + + + + Retrieves information about a specific item associated with this customer. Items represent quantifiable resources such as credits, API calls, storage quotas, or subscription allowances. + + ### Parameters + + + The ID of the item to retrieve, as configured in your Stack Auth project settings. + + + ### Returns + + `Promise`: An [`Item`](../types/item.mdx#item) object containing the display name, current quantity, and other details. + + + + + ```typescript + declare function getItem(itemId: string): Promise; + ``` + + + ```typescript Checking user credits + const user = useUser({ or: "redirect" }); + + const checkCredits = async () => { + const credits = await user.getItem("credits"); + console.log(`Available credits: ${credits.nonNegativeQuantity}`); + console.log(`Actual balance: ${credits.quantity}`); + }; + ``` + + ```typescript Checking team API quota + const team = await user.getTeam("team_123"); + const apiQuota = await team.getItem("api_calls"); + + if (apiQuota.nonNegativeQuantity < 100) { + console.warn("Team is running low on API calls"); + } + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Retrieves information about a specific item associated with this customer, used as a React hook. This provides real-time updates when the item quantity changes. + + ### Parameters + + + The ID of the item to retrieve. + + + ### Returns + + `Item`: An [`Item`](../types/item.mdx#item) object containing the display name, current quantity, and other details. + + + + + ```typescript + declare function useItem(itemId: string): Item; + ``` + + + ```typescript Real-time credits display + function CreditsWidget() { + const user = useUser({ or: "redirect" }); + const credits = user.useItem("credits"); + + return ( +
    +

    Available Credits

    +
    + {credits.nonNegativeQuantity} +
    + {credits.displayName} +
    + ); + } + ``` + + ```typescript Team quota monitoring + function TeamQuotaStatus({ teamId }: { teamId: string }) { + const user = useUser({ or: "redirect" }); + const team = user.useTeam(teamId); + const apiCalls = team.useItem("api_calls"); + + const usagePercentage = (apiCalls.quantity / 10000) * 100; + + return ( +
    +
    +
    +
    +

    + {apiCalls.quantity.toLocaleString()} / 10,000 API calls used +

    +
    + ); + } + ``` + + + + +{/* END_PLATFORM */} + +## Usage Notes + +### Payment Flow + +When using `createCheckoutUrl()`, the typical flow is: + +1. **Create checkout URL**: Call `createCheckoutUrl()` with the desired product ID +2. **Redirect to Stripe**: Direct the user to the returned URL +3. **User completes payment**: Stripe handles the payment process +4. **Webhook processing**: Stack Auth receives webhook notifications from Stripe +5. **Item allocation**: Purchased items are automatically added to the customer's account +6. **User returns**: User is redirected back to your application + +### Item Management + +Items are automatically managed through the payment system: + +- **Purchases**: When a user completes a purchase, associated items are automatically added +- **Subscriptions**: Recurring subscriptions automatically replenish items at the specified intervals +- **Manual allocation**: Server-side code can manually adjust item quantities using [`ServerItem`](../types/item.mdx#serveritem) methods + +### Security Considerations + +- **Client-side safety**: All payment operations are designed to be safe for client-side use +- **Server validation**: Critical operations should always be validated on the server side +- **Race conditions**: Use [`tryDecreaseQuantity()`](../types/item.mdx#serveritemtrydecreasequantity) for atomic, race-condition-free item consumption diff --git a/docs/content/docs/sdk/types/item.mdx b/docs/content/docs/sdk/types/item.mdx new file mode 100644 index 0000000000..0948ab1cc1 --- /dev/null +++ b/docs/content/docs/sdk/types/item.mdx @@ -0,0 +1,236 @@ +--- +title: Item +full: true +--- + +Items represent quantifiable resources in your application, such as credits, API calls, storage quotas, or subscription allowances. They can be associated with users, teams, or custom customers and are managed through Stack Auth's payment system. + +On this page: +- [Item](#item) +- [ServerItem](#serveritem) + +--- + +# `Item` + +The `Item` type represents a quantifiable resource that can be consumed or managed within your application. Items are typically obtained through purchases, subscriptions, or manual allocation. + +Items can be retrieved through: +- [`user.getItem()`](../types/user.mdx#currentusergetitem) +- [`user.useItem()`](../types/user.mdx#currentuseruseitem) (React hook) +- [`team.getItem()`](../types/team.mdx#teamgetitem) +- [`team.useItem()`](../types/team.mdx#teamuseitem) (React hook) + +### Table of Contents + + + + + + + The human-readable name of the item as configured in your Stack Auth project settings. + + + + ```typescript + declare const displayName: string; + ``` + + + + + + + + The current quantity of the item. This value can be negative, which is useful for tracking overdrafts or pending charges. + + For example, if a user has 100 credits but makes a purchase that costs 150 credits, the quantity might temporarily be -50 until the purchase is processed. + + + + ```typescript + declare const quantity: number; + ``` + + + + + + + + The quantity clamped to a minimum of 0. This is equivalent to `Math.max(0, quantity)` and is useful for display purposes when you don't want to show negative values to users. + + Use this when you want to display available resources without confusing users with negative numbers. + + + + ```typescript + declare const nonNegativeQuantity: number; + ``` + + + + +--- + +
    + +# `ServerItem` + +The `ServerItem` type extends `Item` with additional server-side methods for modifying quantities. This type is only available in server-side contexts and provides race-condition-safe operations for managing item quantities. + +Server items can be retrieved through: +- [`serverUser.getItem()`](../types/user.mdx#serverusergetitem) +- [`serverUser.useItem()`](../types/user.mdx#serveruseruseitem) (React hook) +- [`serverTeam.getItem()`](../types/team.mdx#serverteamgetitem) +- [`serverTeam.useItem()`](../types/team.mdx#serverteamuseitem) (React hook) + +### Table of Contents + +; //$stack-link-to:#serveritemincreasequantity + decreaseQuantity(amount): Promise; //$stack-link-to:#serveritemdecreasequantity + tryDecreaseQuantity(amount): Promise; //$stack-link-to:#serveritemtrydecreasequantity + };`} /> + + + + + Increases the item quantity by the specified amount. This operation is atomic and safe for concurrent use. + + ### Parameters + + + The amount to increase the quantity by. Must be a positive number. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function increaseQuantity(amount: number): Promise; + ``` + + + ```typescript Adding credits to a user + const user = await stackServerApp.getUser({ userId: "user_123" }); + const credits = await user.getItem("credits"); + + // Add 100 credits + await credits.increaseQuantity(100); + ``` + + + + + + + + + Decreases the item quantity by the specified amount. This operation allows the quantity to go negative. + + **Note**: If you want to prevent the quantity from going below zero, use [`tryDecreaseQuantity()`](#serveritemtrydecreasequantity) instead, as it provides race-condition-free protection against negative quantities. + + ### Parameters + + + The amount to decrease the quantity by. Must be a positive number. + + + ### Returns + + `Promise` + + + + + ```typescript + declare function decreaseQuantity(amount: number): Promise; + ``` + + + ```typescript Consuming user credits + const user = await stackServerApp.getUser({ userId: "user_123" }); + const credits = await user.getItem("credits"); + + // Consume 50 credits (allows negative balance) + await credits.decreaseQuantity(50); + ``` + + + + + + + + + Attempts to decrease the item quantity by the specified amount, but only if the result would be non-negative. Returns `true` if the operation succeeded, `false` if it would result in a negative quantity. + + This method is race-condition-safe and is ideal for implementing prepaid credit systems where you need to ensure sufficient balance before allowing an operation. + + ### Parameters + + + The amount to decrease the quantity by. Must be a positive number. + + + ### Returns + + `Promise`: `true` if the quantity was successfully decreased, `false` if the operation would result in a negative quantity. + + + + + ```typescript + declare function tryDecreaseQuantity(amount: number): Promise; + ``` + + + ```typescript Safe credit consumption + const user = await stackServerApp.getUser({ userId: "user_123" }); + const credits = await user.getItem("credits"); + + // Try to consume 50 credits, only if sufficient balance + const success = await credits.tryDecreaseQuantity(50); + + if (success) { + console.log("Credits consumed successfully"); + // Proceed with the operation + } else { + console.log("Insufficient credits"); + // Handle insufficient balance + throw new Error("Not enough credits available"); + } + ``` + + ```typescript API rate limiting with credits + async function handleApiCall(userId: string) { + const user = await stackServerApp.getUser({ userId }); + const apiCalls = await user.getItem("api_calls"); + + // Check if user has API calls remaining + const canProceed = await apiCalls.tryDecreaseQuantity(1); + + if (!canProceed) { + throw new Error("API rate limit exceeded. Please upgrade your plan."); + } + + // Process the API call + return processApiRequest(); + } + ``` + + + + diff --git a/docs/content/docs/sdk/types/project.mdx b/docs/content/docs/sdk/types/project.mdx index ffadb921c2..cbb2079f87 100644 --- a/docs/content/docs/sdk/types/project.mdx +++ b/docs/content/docs/sdk/types/project.mdx @@ -5,7 +5,7 @@ full: true The `Project` object contains the information and configuration of a project, such as the name, description, and enabled authentication methods. -Each [Stack app](../../concepts/stack-app) corresponds to a project. You can obtain its `Project` object by calling [`stackApp.getProject()`](../objects/stack-app.mdx#stackappgetproject) +Each [Stack app](../../concepts/stack-app.mdx) corresponds to a project. You can obtain its `Project` object by calling [`stackApp.getProject()`](../objects/stack-app.mdx#stackappgetproject) or [`stackApp.useProject()`](../objects/stack-app.mdx#stackappuseproject) {/* THIS_LINE_PLATFORM react-like */} . diff --git a/docs/content/docs/sdk/types/team.mdx b/docs/content/docs/sdk/types/team.mdx index 372ba46c84..a9071b6875 100644 --- a/docs/content/docs/sdk/types/team.mdx +++ b/docs/content/docs/sdk/types/team.mdx @@ -28,8 +28,8 @@ You can get `Team` objects with the clientMetadata: Json; //$stack-link-to:#teamclientmetadata clientReadOnlyMetadata: Json; //$stack-link-to:#teamclientreadonlymetadata - update(data): Promise; //$stack-link-to:#teamupdatedata - inviteUser(options): Promise; //$stack-link-to:#teaminviteuseroptions + update(data): Promise; //$stack-link-to:#teamupdate + inviteUser(options): Promise; //$stack-link-to:#teaminviteuser listUsers(): Promise; //$stack-link-to:#teamlistusers // NEXT_LINE_PLATFORM react-like ⤷ useUsers(): TeamUser[]; //$stack-link-to:#teamuseusers @@ -37,10 +37,15 @@ You can get `Team` objects with the // NEXT_LINE_PLATFORM react-like ⤷ useInvitations(): { ... }[]; //$stack-link-to:#teamuseinvitations - createApiKey(options): Promise; //$stack-link-to:#teamcreateapikeyoptions + createApiKey(options): Promise; //$stack-link-to:#teamcreateapikey listApiKeys(): Promise; //$stack-link-to:#teamlistapikeys // NEXT_LINE_PLATFORM react-like ⤷ useApiKeys(): TeamApiKey[]; //$stack-link-to:#teamuseapikeys + + createCheckoutUrl(options): Promise; //$stack-link-to:#teamcreatecheckouturl + getItem(itemId): Promise; //$stack-link-to:#teamgetitem + // NEXT_LINE_PLATFORM react-like + ⤷ useItem(itemId): Item; //$stack-link-to:#teamuseitem };`} /> @@ -476,6 +481,130 @@ You can get `Team` objects with the {/* END_PLATFORM */} + + + + Creates a checkout URL for the team to purchase products. This method integrates with Stripe to generate a secure payment link for team-level purchases. + + Note that this operation requires the current user to have appropriate permissions for team purchases. The specific permission requirements depend on your project configuration. + + ### Parameters + + + Options for creating the checkout URL. + + + The ID of the product to purchase. + + + + + ### Returns + + `Promise`: A URL that redirects to the Stripe checkout page for the specified product. + + + + + ```typescript + declare function createCheckoutUrl(options: { + productId: string; + }): Promise; + ``` + + + ```typescript Team purchasing additional seats + const checkoutUrl = await team.createCheckoutUrl({ + productId: "prod_team_seats", + }); + + // Redirect to checkout + window.location.href = checkoutUrl; + ``` + + + + + + + + + Retrieves information about a specific item (such as credits, API quotas, storage limits, etc.) for the team. + + ### Parameters + + + The ID of the item to retrieve. + + + ### Returns + + `Promise`: The item object containing display name, quantity, and other details. + + + + + ```typescript + declare function getItem(itemId: string): Promise; + ``` + + + ```typescript Checking team API quota + const apiQuota = await team.getItem("api_calls"); + console.log(`Team has ${apiQuota.quantity} API calls remaining`); + + if (apiQuota.nonNegativeQuantity < 100) { + console.warn("Team is running low on API calls"); + } + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Retrieves information about a specific item for the team, used as a React hook. + + ### Parameters + + + The ID of the item to retrieve. + + + ### Returns + + `Item`: The item object containing display name, quantity, and other details. + + + + + ```typescript + declare function useItem(itemId: string): Item; + ``` + + + ```typescript Team quota monitoring component + function TeamQuotaDisplay({ team }: { team: Team }) { + const storage = team.useItem("storage_gb"); + + return ( +
    +

    Team Storage Usage

    +

    {storage.quantity} GB used

    +

    Plan: {storage.displayName}

    +
    + ); + } + ``` +
    +
    +
    +
    +{/* END_PLATFORM */} + --- # `ServerTeam` diff --git a/docs/content/docs/sdk/types/user.mdx b/docs/content/docs/sdk/types/user.mdx index 0889748aa4..b95e7172ef 100644 --- a/docs/content/docs/sdk/types/user.mdx +++ b/docs/content/docs/sdk/types/user.mdx @@ -64,6 +64,11 @@ You can call `useUser()` or `stackServerApp.getUser()` to get the `CurrentUser` listApiKeys(): Promise; //$stack-link-to:#currentuserlistapikeys // NEXT_LINE_PLATFORM react-like ⤷ useApiKeys(): UserApiKey[]; //$stack-link-to:#currentuseruseapikeys + + createCheckoutUrl(options): Promise; //$stack-link-to:#currentusercreatecheckouturl + getItem(itemId): Promise; //$stack-link-to:#currentusergetitem + // NEXT_LINE_PLATFORM react-like + ⤷ useItem(itemId): Item; //$stack-link-to:#currentuseruseitem };`} /> @@ -1560,6 +1565,125 @@ The `ServerUser` object contains most `CurrentUser` properties and methods with {/* END_PLATFORM */} + + + + Creates a checkout URL for purchasing a product. This method integrates with Stripe to generate a secure payment link. + + ### Parameters + + + Options for creating the checkout URL. + + + The ID of the product to purchase. + + + + + ### Returns + + `Promise`: A URL that redirects to the Stripe checkout page for the specified product. + + + + + ```typescript + declare function createCheckoutUrl(options: { + productId: string; + }): Promise; + ``` + + + ```typescript Creating a checkout URL + const checkoutUrl = await user.createCheckoutUrl({ + productId: "prod_premium_plan", + }); + + // Redirect user to checkout + window.location.href = checkoutUrl; + ``` + + + + + + + + + Retrieves information about a specific item (such as credits, subscription quantities, etc.) for the user. + + ### Parameters + + + The ID of the item to retrieve. + + + ### Returns + + `Promise`: The item object containing display name, quantity, and other details. + + + + + ```typescript + declare function getItem(itemId: string): Promise; + ``` + + + ```typescript Getting user credits + const credits = await user.getItem("credits"); + console.log(`User has ${credits.quantity} credits`); + console.log(`Non-negative quantity: ${credits.nonNegativeQuantity}`); + ``` + + + + + +{/* IF_PLATFORM next */} + + + + Retrieves information about a specific item for the user, used as a React hook. + + ### Parameters + + + The ID of the item to retrieve. + + + ### Returns + + `Item`: The item object containing display name, quantity, and other details. + + + + + ```typescript + declare function useItem(itemId: string): Item; + ``` + + + ```typescript Using credits in a React component + function CreditsDisplay() { + const user = useUser(); + const credits = user.useItem("credits"); + + return ( +
    +

    Available Credits: {credits.quantity}

    +

    Display Name: {credits.displayName}

    +
    + ); + } + ``` +
    +
    +
    +
    +{/* END_PLATFORM */} + ---
    From bed02655555dd2f8f0e4182f1782ae3bc528d99a Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 14:24:09 -0500 Subject: [PATCH 45/55] update overview --- docs/content/docs/(guides)/rest-api/overview.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/(guides)/rest-api/overview.mdx b/docs/content/docs/(guides)/rest-api/overview.mdx index 44127cf235..1d59d67a07 100644 --- a/docs/content/docs/(guides)/rest-api/overview.mdx +++ b/docs/content/docs/(guides)/rest-api/overview.mdx @@ -10,9 +10,9 @@ Stack offers a REST API for backends & frontends of any programming language or Stack Auth uses different authentication patterns depending on whether you're making requests from client-side code (browser, mobile app) or server-side code (your backend). - + **Security Critical**: Never expose your secret server key (`ssk_...`) in client-side code, browser requests, or any publicly accessible location. Server keys should only be used in secure backend environments. - + ### Client-Side Authentication From cfd4bf189356da2e6c26085f3e957e7f1d444874 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 14 Oct 2025 14:27:52 -0500 Subject: [PATCH 46/55] update currentUser.updatePassword sdk --- docs/content/docs/sdk/types/user.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/content/docs/sdk/types/user.mdx b/docs/content/docs/sdk/types/user.mdx index b95e7172ef..062228d0b3 100644 --- a/docs/content/docs/sdk/types/user.mdx +++ b/docs/content/docs/sdk/types/user.mdx @@ -983,7 +983,7 @@ You can call `useUser()` or `stackServerApp.getUser()` to get the `CurrentUser` ### Returns - `Promise` + `Promise`: Returns an error object if the operation fails, otherwise returns `void`. @@ -992,19 +992,19 @@ You can call `useUser()` or `stackServerApp.getUser()` to get the `CurrentUser` declare function updatePassword(data: { oldPassword: string; newPassword: string; - }): Promise; + }): Promise; ``` ```typescript Updating user password - const result = await user.updatePassword({ + const error = await user.updatePassword({ oldPassword: "currentPassword", newPassword: "newPassword", }); - if (result.status === "error") { - console.error("Error updating password", result.error); + if (error) { + console.error("Error updating password", error); } else { - console.log("Password updated"); + console.log("Password updated successfully"); } ``` From 4acef8ebc1aa68b0c230b5e5d062aa88f729eef9 Mon Sep 17 00:00:00 2001 From: Madison Date: Wed, 15 Oct 2025 02:23:03 -0500 Subject: [PATCH 47/55] minor fixes --- .../page-examples/password-reset.mdx | 6 +- docs/content/docs/(guides)/overview.mdx | 6 +- .../docs/components/credential-sign-in.mdx | 2 +- .../docs/components/credential-sign-up.mdx | 2 +- docs/content/docs/components/index.mdx | 2 +- docs/content/docs/components/oauth-button.mdx | 2 +- .../components/selected-team-switcher.mdx | 2 +- docs/content/docs/components/sign-in.mdx | 2 +- docs/content/docs/components/sign-up.mdx | 2 +- .../content/docs/components/stack-handler.mdx | 2 +- .../docs/components/stack-provider.mdx | 2 +- docs/content/docs/components/stack-theme.mdx | 2 +- docs/content/docs/sdk/types/user.mdx | 246 +++++++++--------- docs/src/components/chat/ai-chat.tsx | 2 +- .../src/components/mdx/platform-codeblock.tsx | 11 +- 15 files changed, 151 insertions(+), 140 deletions(-) diff --git a/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx b/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx index e10681337d..f3678a28f1 100644 --- a/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx +++ b/docs/content/docs/(guides)/customization/page-examples/password-reset.mdx @@ -70,7 +70,11 @@ export default function CustomPasswordResetForm({ code }: { code: string }) { } setSuccess(true); } catch (err) { - setError(`An unexpected error occurred: ${err.message}`); + if (err instanceof Error) { + setError(`An unexpected error occurred: ${err.message}`); + } else { + setError('An unexpected error occurred'); + } } }; diff --git a/docs/content/docs/(guides)/overview.mdx b/docs/content/docs/(guides)/overview.mdx index 08e8186d31..095a804fb9 100644 --- a/docs/content/docs/(guides)/overview.mdx +++ b/docs/content/docs/(guides)/overview.mdx @@ -4,7 +4,7 @@ title: Overview Welcome to Stack Auth, the developer-friendly open-source authentication platform that gets you started in minutes! -You can get started in five minutes with our [setup guide](./getting-started/setup.mdx), or jump straight into the documentation. +You can get started in five minutes with our [setup guide](./getting-started/setup), or jump straight into the documentation. diff --git a/docs/content/docs/components/credential-sign-in.mdx b/docs/content/docs/components/credential-sign-in.mdx index c1e1ffa9a3..a72c783061 100644 --- a/docs/content/docs/components/credential-sign-in.mdx +++ b/docs/content/docs/components/credential-sign-in.mdx @@ -10,7 +10,7 @@ A component that renders a sign-in form with email and password fields. Note that if credential sign-in is disabled in the dashboard, this component will still render. However, attempting to use it will result in an error being thrown. -For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). +For more information, please refer to the [custom pages guide](../customization/custom-pages). ## Props diff --git a/docs/content/docs/components/credential-sign-up.mdx b/docs/content/docs/components/credential-sign-up.mdx index d804d0a933..fbf42de404 100644 --- a/docs/content/docs/components/credential-sign-up.mdx +++ b/docs/content/docs/components/credential-sign-up.mdx @@ -10,7 +10,7 @@ A component that renders a sign-up form with email and password fields. Note that if credential sign-up is disabled in the dashboard, this component will still render. However, attempting to use it will result in an error being thrown. -For more information, please refer to the [custom pages guide](../customization/custom-pages.mdx). +For more information, please refer to the [custom pages guide](../customization/custom-pages). ## Props diff --git a/docs/content/docs/components/index.mdx b/docs/content/docs/components/index.mdx index 9df168d153..db05ce81f2 100644 --- a/docs/content/docs/components/index.mdx +++ b/docs/content/docs/components/index.mdx @@ -4,7 +4,7 @@ title: Components Overview Stack Auth provides a set of components for Next.js applications. -To get started with Stack Auth in your Next.js application, follow the [setup guide](../getting-started/setup.mdx). To see the hooks and objects in the Next.js SDK, see the [SDK reference](../sdk). +To get started with Stack Auth in your Next.js application, follow the [setup guide](./getting-started/setup). To see the hooks and objects in the Next.js SDK, see the [SDK reference](./sdk).
    - - -

    OAuth Authentication

    -

    ← Back to home

    - -
    -

    Sign In with OAuth

    - -
    - - - - - ``` - - - - ```typescript - import { stackClientApp } from "./stack/client"; - - // Check if user is already signed in - stackClientApp.getUser().then((user) => { - if (user) { - window.location.href = "/"; - } - }); - - // Handle Google Sign In - document.getElementById("googleSignIn")?.addEventListener("click", async () => { - try { - await stackClientApp.signInWithOAuth('google'); - } catch (error) { - console.error("Google sign in failed:", error); - alert("Failed to initialize Google sign in"); - } - }); - - // Handle OAuth redirect - window.addEventListener("load", async () => { - try { - const params = new URLSearchParams(window.location.search); - const code = params.get("code"); - const state = params.get("state"); - - if (code && state) { - const user = await stackClientApp.callOAuthCallback(); - if (user) { - window.location.href = "/"; - } - } - } catch (error) { - console.error("Failed to handle OAuth redirect:", error); - alert("Authentication failed. Please try again."); - } - }); - ``` - - From 57854321850cbe060704c6a85814e87eb594c222 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 20 Oct 2025 04:31:50 -0500 Subject: [PATCH 53/55] redirect homepage to docs/overview --- docs/next.config.mjs | 6 ++++++ docs/src/app/(home)/page.tsx | 42 +++--------------------------------- 2 files changed, 9 insertions(+), 39 deletions(-) diff --git a/docs/next.config.mjs b/docs/next.config.mjs index 181508d152..43dc85ca3e 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -15,6 +15,12 @@ const config = { }, async redirects() { return [ + // Redirect root to docs overview + { + source: '/', + destination: '/docs/overview', + permanent: false, + }, // Redirect /docs/api to the overview page { source: '/docs/api', diff --git a/docs/src/app/(home)/page.tsx b/docs/src/app/(home)/page.tsx index e0af3b40c4..bd9d783d40 100644 --- a/docs/src/app/(home)/page.tsx +++ b/docs/src/app/(home)/page.tsx @@ -1,41 +1,5 @@ -import DocsSelector from '@/components/homepage/iconHover'; - +// This page is never reached because of the redirect in next.config.mjs +// Kept as a fallback in case the redirect configuration is removed export default function HomePage() { - return ( -
    - {/* Hero Section */} -
    -
    -
    - - - -
    - -

    - Stack Auth - - Documentation - -

    - -

    - Complete authentication solution with comprehensive guides, API references, and platform-specific examples to get you started quickly. -

    - - {/* Documentation Type Selection */} -
    - -
    -
    -
    -
    - ); + return null; } From b30a46849c804c26b5416e980dfaab984c3b7a26 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 20 Oct 2025 04:55:35 -0500 Subject: [PATCH 54/55] update code examples --- docs/code-examples/setup.ts | 23 +++++++++++++++++++ .../docs/(guides)/getting-started/setup.mdx | 12 ++++++++++ 2 files changed, 35 insertions(+) diff --git a/docs/code-examples/setup.ts b/docs/code-examples/setup.ts index cfe09acdbb..88d8cee772 100644 --- a/docs/code-examples/setup.ts +++ b/docs/code-examples/setup.ts @@ -404,6 +404,29 @@ export default function RootLayout({ children }: { children: React.ReactNode }) } ] as CodeExample[], + 'suspense-boundary': [ + { + language: 'JavaScript', + framework: 'React', + code: `import { Suspense } from "react"; +import { StackProvider } from "@stackframe/react"; +import { stackClientApp } from "./stack/client"; + +export default function App() { + return ( + // Wrap your StackProvider with Suspense for async hooks to work + Loading...
    }> + + {/* Your app content */} + + + ); +}`, + highlightLanguage: 'typescript', + filename: 'App.tsx' + } + ] as CodeExample[], + 'test-setup': [ { language: 'JavaScript', diff --git a/docs/content/docs/(guides)/getting-started/setup.mdx b/docs/content/docs/(guides)/getting-started/setup.mdx index 0a0a5cce6e..33c0b9052b 100644 --- a/docs/content/docs/(guides)/getting-started/setup.mdx +++ b/docs/content/docs/(guides)/getting-started/setup.mdx @@ -153,6 +153,18 @@ We recommend using our **setup wizard** for JavaScript frameworks for a seamless title="Loading Component" /> + + ### Add suspense boundary (React only) + + + For React, add a suspense boundary: + + + ### Done! From cc29fc30d173e9df5d59dc733121b45968c589f7 Mon Sep 17 00:00:00 2001 From: Madison Date: Mon, 20 Oct 2025 04:57:58 -0500 Subject: [PATCH 55/55] default toc open on larger screens --- .../components/layouts/sidebar-context.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/src/components/layouts/sidebar-context.tsx b/docs/src/components/layouts/sidebar-context.tsx index 338f77c5dc..01daf164f6 100644 --- a/docs/src/components/layouts/sidebar-context.tsx +++ b/docs/src/components/layouts/sidebar-context.tsx @@ -54,15 +54,26 @@ export function SidebarProvider({ children }: { children: ReactNode }) { const [isFullPage, setIsFullPage] = useState(false); const [isMainSidebarCollapsed, setIsMainSidebarCollapsed] = useState(false); - // Load state from localStorage on mount + // Load state from localStorage on mount and set defaults useEffect(() => { + const savedToc = localStorage.getItem('toc-open'); const savedChat = localStorage.getItem('ai-chat-open'); const savedExpanded = localStorage.getItem('ai-chat-expanded'); const savedMainSidebarCollapsed = localStorage.getItem('main-sidebar-collapsed'); + // Check if on large screen (768px+) + const isLargeScreen = window.innerWidth >= 768; + + // Priority: saved state > default TOC on large screens > nothing if (savedChat === 'true') { setActiveSidebar('chat'); + } else if (savedToc === 'true') { + setActiveSidebar('toc'); + } else if (isLargeScreen && savedToc !== 'false') { + // Default to TOC on large screens if no preference is saved + setActiveSidebar('toc'); } + if (savedExpanded === 'true') { setIsChatExpanded(true); } @@ -102,7 +113,13 @@ export function SidebarProvider({ children }: { children: ReactNode }) { // Individual controls const setTocOpen = (open: boolean) => { - setActiveSidebar(open ? 'toc' : null); + if (open) { + setActiveSidebar('toc'); + localStorage.setItem('toc-open', 'true'); + } else { + setActiveSidebar(null); + localStorage.setItem('toc-open', 'false'); + } }; const setChatOpen = (open: boolean) => {