Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b943873
feat(nuxt): Introduce keyless mode
wobsoriano Feb 11, 2026
01c26c4
chore: clean up keyless logic
wobsoriano Feb 13, 2026
e80b974
chore: remove unused type
wobsoriano Feb 13, 2026
ac444ee
chore: add changeset
wobsoriano Feb 13, 2026
f8e080d
Merge branch 'main' into rob/nuxt-keyless
wobsoriano Mar 16, 2026
2ae15e6
fix(nuxt): add missing tsup entry points for keyless mode files
wobsoriano Mar 16, 2026
6e0497f
fix(integration): update nuxt template to 4.4.2
wobsoriano Mar 16, 2026
7134248
Merge branch 'main' into rob/nuxt-keyless
wobsoriano Mar 16, 2026
e476a00
test: bump nuxt template versions
wobsoriano Mar 16, 2026
4c3d660
chore: remove unused files
wobsoriano Mar 16, 2026
71566de
fix(integration): run pnpm install after pkglab add to fix dependency…
wobsoriano Mar 16, 2026
587cf4f
revert: remove pnpm install after pkglab add
wobsoriano Mar 16, 2026
72fa493
test fix
wobsoriano Mar 16, 2026
9bd3d89
Merge branch 'main' into rob/nuxt-keyless
wobsoriano Mar 16, 2026
1f723b6
fix: Move runtime config imports
wobsoriano Mar 17, 2026
5f8e1cc
chore: remove compat and unused await
wobsoriano Mar 17, 2026
b5653fa
fix(nuxt): support NUXT_CLERK_API_URL for private runtime config
wobsoriano Mar 17, 2026
a1223fe
fix(nuxt): move apiUrl from public to private runtime config
wobsoriano Mar 17, 2026
836c54d
fix(nuxt): restore apiUrl as public runtime config
wobsoriano Mar 17, 2026
9e8186d
revert: restore original integration env settings
wobsoriano Mar 17, 2026
61070c1
fix(nuxt): move apiUrl and apiVersion to private runtime config
wobsoriano Mar 17, 2026
c7f39e9
fix(nuxt): deprecate NUXT_PUBLIC_CLERK_API_URL in favor of NUXT_CLERK…
wobsoriano Mar 17, 2026
3416006
Merge branch 'main' into rob/nuxt-keyless
wobsoriano Mar 17, 2026
d29eba9
chore: add changeset
wobsoriano Mar 17, 2026
ad8328e
Merge branch 'main' into rob/nuxt-keyless
wobsoriano Apr 1, 2026
ec0db85
Merge branch 'main' into rob/nuxt-keyless
wobsoriano Apr 9, 2026
c37d62c
refactor(nuxt): use ClerkKeylessContext type in plugin
wobsoriano Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-turkeys-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/nuxt": minor
---

Introduce Keyless quickstart for Nuxt. This allows the Clerk SDK to be used without having to sign up and paste your keys manually.
6 changes: 6 additions & 0 deletions .changeset/tiny-papayas-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clerk/astro": patch
"@clerk/react-router": patch
---

Simplified keyless service initialization.
6 changes: 3 additions & 3 deletions integration/templates/nuxt-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"preview": "nuxt preview --port $PORT"
},
"dependencies": {
"nuxt": "4.1.2",
"vue": "^3.5.13",
"vue-router": "^4.4.5"
"nuxt": "4.4.2",
"vue": "^3.5.30",
"vue-router": "^5.0.3"
}
}
55 changes: 55 additions & 0 deletions integration/tests/nuxt/keyless.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test } from '@playwright/test';

import type { Application } from '../../models/application';
import { appConfigs } from '../../presets';
import {
testClaimedAppWithMissingKeys,
testKeylessRemovedAfterEnvAndRestart,
testToggleCollapsePopoverAndClaim,
} from '../../testUtils/keylessHelpers';

const commonSetup = appConfigs.nuxt.node.clone();

test.describe('Keyless mode @nuxt', () => {
test.describe.configure({ mode: 'serial' });
test.setTimeout(90_000);

test.use({
extraHTTPHeaders: {
'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET || '',
},
});

let app: Application;
let dashboardUrl = 'https://dashboard.clerk.com/';

test.beforeAll(async () => {
app = await commonSetup.commit();
await app.setup();
await app.withEnv(appConfigs.envs.withKeyless);
if (appConfigs.envs.withKeyless.privateVariables.get('CLERK_API_URL')?.includes('clerkstage')) {
dashboardUrl = 'https://dashboard.clerkstage.dev/';
}
await app.dev();
});

test.afterAll(async () => {
// Keep files for debugging
await app?.teardown();
});

test('Toggle collapse popover and claim.', async ({ page, context }) => {
await testToggleCollapsePopoverAndClaim({ page, context, app, dashboardUrl, framework: 'nuxt' });
});

test('Lands on claimed application with missing explicit keys, expanded by default, click to get keys from dashboard.', async ({
page,
context,
}) => {
await testClaimedAppWithMissingKeys({ page, context, app, dashboardUrl });
});

test('Keyless popover is removed after adding keys to .env and restarting.', async ({ page, context }) => {
await testKeylessRemovedAfterEnvAndRestart({ page, context, app });
});
});
106 changes: 30 additions & 76 deletions packages/astro/src/server/keyless/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,37 @@ import type { APIContext } from 'astro';
import { clerkClient } from '../clerk-client';
import { createFileStorage } from './file-storage.js';

// Lazily initialized keyless service singleton
let keylessServiceInstance: ReturnType<typeof createKeylessService> | null = null;
let keylessInitPromise: Promise<ReturnType<typeof createKeylessService> | null> | null = null;

function canUseFileSystem(): boolean {
try {
return typeof process !== 'undefined' && typeof process.cwd === 'function';
} catch {
return false;
}
}

/**
* Gets or creates the keyless service singleton.
* Returns null for non-Node.js runtimes (e.g., Cloudflare Workers).
*/
export async function keyless(context: APIContext): Promise<ReturnType<typeof createKeylessService> | null> {
if (!canUseFileSystem()) {
return null;
}

if (keylessServiceInstance) {
return keylessServiceInstance;
}

if (keylessInitPromise) {
return keylessInitPromise;
}

keylessInitPromise = (async () => {
try {
const storage = await createFileStorage();

const service = createKeylessService({
storage,
api: {
async createAccountlessApplication(requestHeaders?: Headers) {
try {
return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({
requestHeaders,
});
} catch {
return null;
}
},
async completeOnboarding(requestHeaders?: Headers) {
try {
return await clerkClient(
context,
).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({
requestHeaders,
});
} catch {
return null;
}
},
export function keyless(context: APIContext) {
if (!keylessServiceInstance) {
keylessServiceInstance = createKeylessService({
storage: createFileStorage(),
api: {
async createAccountlessApplication(requestHeaders?: Headers) {
try {
return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({
requestHeaders,
});
} catch {
return null;
}
},
framework: 'astro',
frameworkVersion: PACKAGE_VERSION,
});

keylessServiceInstance = service;
return service;
} catch (error) {
console.warn('[Clerk] Failed to initialize keyless service:', error);
return null;
} finally {
keylessInitPromise = null;
}
})();

return keylessInitPromise;
}

/**
* @internal
*/
export function resetKeylessService(): void {
keylessServiceInstance = null;
keylessInitPromise = null;
async completeOnboarding(requestHeaders?: Headers) {
try {
return await clerkClient(
context,
).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({
requestHeaders,
});
} catch {
return null;
}
},
},
framework: 'astro',
});
}
return keylessServiceInstance;
}
8 changes: 6 additions & 2 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ export default defineNuxtModule<ModuleOptions>({
// Backend specific variables that are safe to share.
// We want them to be overridable like the other public keys (e.g NUXT_PUBLIC_CLERK_PROXY_URL)
proxyUrl: options.proxyUrl,
apiUrl: '',
apiVersion: 'v1',
// Deprecated: use NUXT_CLERK_API_URL and NUXT_CLERK_API_VERSION instead.
// Kept for backwards compatibility with NUXT_PUBLIC_CLERK_API_URL / NUXT_PUBLIC_CLERK_API_VERSION.
apiUrl: undefined,
apiVersion: undefined,
},
},
// Private keys available only on within server-side
Expand All @@ -84,6 +86,8 @@ export default defineNuxtModule<ModuleOptions>({
machineSecretKey: undefined,
jwtKey: undefined,
webhookSigningSecret: undefined,
apiUrl: undefined,
apiVersion: undefined,
},
});

Expand Down
11 changes: 11 additions & 0 deletions packages/nuxt/src/runtime/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import { clerkPlugin } from '@clerk/vue';
import { setErrorThrowerOptions } from '@clerk/vue/internal';
import { defineNuxtPlugin, navigateTo, useRuntimeConfig, useState } from 'nuxt/app';

import type { ClerkKeylessContext } from './server/types';

setErrorThrowerOptions({ packageName: PACKAGE_NAME });
setClerkJSLoadingErrorPackageName(PACKAGE_NAME);

export default defineNuxtPlugin(nuxtApp => {
// SSR-friendly shared state
const initialState = useState<InitialState | undefined>('clerk-initial-state', () => undefined);
const keylessContext = useState<ClerkKeylessContext | undefined>('clerk-keyless-context', () => undefined);

if (import.meta.server) {
// Save the initial state from server and pass it to the plugin
initialState.value = nuxtApp.ssrContext?.event.context.__clerk_initial_state;
keylessContext.value = nuxtApp.ssrContext?.event.context.__clerk_keyless;
}

const runtimeConfig = useRuntimeConfig();
Expand All @@ -34,5 +38,12 @@ export default defineNuxtPlugin(nuxtApp => {
routerPush: (to: string) => navigateTo(to),
routerReplace: (to: string) => navigateTo(to, { replace: true }),
initialState: initialState.value,
// Add keyless mode props if present
...(keylessContext.value
? {
__internal_keyless_claimKeylessApplicationUrl: keylessContext.value.claimUrl,
__internal_keyless_copyInstanceKeysUrl: keylessContext.value.apiKeysUrl,
}
: {}),
});
});
31 changes: 26 additions & 5 deletions packages/nuxt/src/runtime/server/clerkClient.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import { createClerkClient } from '@clerk/backend';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { deprecated } from '@clerk/shared/deprecated';
import { isTruthy } from '@clerk/shared/underscore';
import type { H3Event } from 'h3';

// @ts-expect-error: Nitro import. Handled by Nuxt.
import { useRuntimeConfig } from '#imports';

function resolveApiUrl(runtimeConfig: ReturnType<typeof useRuntimeConfig>): string {
if (runtimeConfig.clerk.apiUrl) {
return runtimeConfig.clerk.apiUrl;
}
if (runtimeConfig.public.clerk.apiUrl) {
deprecated('NUXT_PUBLIC_CLERK_API_URL', 'Use `NUXT_CLERK_API_URL` instead.');
return runtimeConfig.public.clerk.apiUrl;
}
return apiUrlFromPublishableKey(runtimeConfig.public.clerk.publishableKey);
}

function resolveApiVersion(runtimeConfig: ReturnType<typeof useRuntimeConfig>): string {
if (runtimeConfig.clerk.apiVersion) {
return runtimeConfig.clerk.apiVersion;
}
if (runtimeConfig.public.clerk.apiVersion) {
deprecated('NUXT_PUBLIC_CLERK_API_VERSION', 'Use `NUXT_CLERK_API_VERSION` instead.');
return runtimeConfig.public.clerk.apiVersion;
}
return 'v1';
}

export function clerkClient(event: H3Event) {
const runtimeConfig = useRuntimeConfig(event);
const publishableKey = runtimeConfig.public.clerk.publishableKey;
const apiUrl = runtimeConfig.public.clerk.apiUrl || apiUrlFromPublishableKey(publishableKey);

return createClerkClient({
publishableKey,
apiUrl,
apiVersion: runtimeConfig.public.clerk.apiVersion,
publishableKey: runtimeConfig.public.clerk.publishableKey,
apiUrl: resolveApiUrl(runtimeConfig),
apiVersion: resolveApiVersion(runtimeConfig),
proxyUrl: runtimeConfig.public.clerk.proxyUrl,
domain: runtimeConfig.public.clerk.domain,
isSatellite: runtimeConfig.public.clerk.isSatellite,
Expand Down
42 changes: 42 additions & 0 deletions packages/nuxt/src/runtime/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import type { PendingSessionOptions } from '@clerk/shared/types';
import type { EventHandler } from 'h3';
import { createError, eventHandler, setResponseHeader } from 'h3';

// @ts-expect-error: Nitro import. Handled by Nuxt.
import { useRuntimeConfig } from '#imports';

import { canUseKeyless } from '../utils/feature-flags';
import { clerkClient } from './clerkClient';
import { resolveKeysWithKeylessFallback } from './keyless/utils';
import type { AuthFn, AuthOptions } from './types';
import { createInitialState, toWebRequest } from './utils';

Expand Down Expand Up @@ -82,6 +87,35 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
return eventHandler(async event => {
const clerkRequest = toWebRequest(event);

// Resolve keyless in development if keys are missing
let keylessClaimUrl: string | undefined;
let keylessApiKeysUrl: string | undefined;

if (canUseKeyless) {
try {
const runtimeConfig = useRuntimeConfig(event);

const { publishableKey, secretKey, claimUrl, apiKeysUrl } = await resolveKeysWithKeylessFallback(
runtimeConfig.public.clerk.publishableKey,
runtimeConfig.clerk.secretKey,
event,
);

keylessClaimUrl = claimUrl;
keylessApiKeysUrl = apiKeysUrl;

// Override runtime config with keyless values if returned
if (publishableKey) {
runtimeConfig.public.clerk.publishableKey = publishableKey;
}
if (secretKey) {
runtimeConfig.clerk.secretKey = secretKey;
}
} catch {
// Silently fail - continue without keyless
}
}

const requestState = await clerkClient(event).authenticateRequest(clerkRequest, {
...options,
acceptsToken: 'any',
Expand Down Expand Up @@ -117,6 +151,14 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
// Internal serializable state that will be passed to the client
event.context.__clerk_initial_state = createInitialState(authObjectFn());

// Store keyless mode URLs in separate context property
if (canUseKeyless && keylessClaimUrl) {
event.context.__clerk_keyless = {
claimUrl: keylessClaimUrl,
apiKeysUrl: keylessApiKeysUrl,
};
}

await handler?.(event);
});
};
19 changes: 19 additions & 0 deletions packages/nuxt/src/runtime/server/keyless/fileStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as fs from 'node:fs';
import * as path from 'node:path';

import { createNodeFileStorage, type KeylessStorage } from '@clerk/shared/keyless';

export type { KeylessStorage };

export interface FileStorageOptions {
cwd?: () => string;
}

export function createFileStorage(options: FileStorageOptions = {}): KeylessStorage {
const { cwd = () => process.cwd() } = options;

return createNodeFileStorage(fs, path, {
cwd,
frameworkPackageName: '@clerk/nuxt',
});
}
Loading
Loading