diff --git a/.changeset/clerk-hono-initial.md b/.changeset/clerk-hono-initial.md
new file mode 100644
index 00000000000..051352e23b8
--- /dev/null
+++ b/.changeset/clerk-hono-initial.md
@@ -0,0 +1,31 @@
+---
+'@clerk/hono': minor
+---
+
+Initial release of `@clerk/hono` - the official Clerk SDK for Hono.
+
+This package provides:
+- `clerkMiddleware()` - Middleware to authenticate requests and attach auth data to Hono context
+- `getAuth(c)` - Helper to retrieve auth data from Hono context
+- `verifyWebhook(c)` - Webhook verification via `@clerk/hono/webhooks`
+
+**Usage:**
+
+```typescript
+import { Hono } from 'hono';
+import { clerkMiddleware, getAuth } from '@clerk/hono';
+
+const app = new Hono();
+
+app.use('*', clerkMiddleware());
+
+app.get('/protected', (c) => {
+ const { userId } = getAuth(c);
+ if (!userId) {
+ return c.json({ error: 'Unauthorized' }, 401);
+ }
+ return c.json({ userId });
+});
+```
+
+Based on the community `@hono/clerk-auth` package. Thank you to Vaggelis Yfantis for the original implementation!
diff --git a/.changeset/config.json b/.changeset/config.json
index ace011cb04b..26976909e27 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -7,6 +7,7 @@
}
],
"commit": false,
+ "ignore": ["@clerk/hono"],
"fixed": [],
"linked": [],
"access": "public",
diff --git a/packages/hono/README.md b/packages/hono/README.md
new file mode 100644
index 00000000000..671c863906c
--- /dev/null
+++ b/packages/hono/README.md
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+# @clerk/hono
+
+
+
+[](https://clerk.com/discord)
+[](https://clerk.com/docs?utm_source=github&utm_medium=clerk_hono)
+[](https://twitter.com/intent/follow?screen_name=ClerkDev)
+
+[Changelog](https://github.com/clerk/javascript/blob/main/packages/hono/CHANGELOG.md)
+·
+[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml)
+·
+[Request a Feature](https://feedback.clerk.com/roadmap)
+·
+[Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_hono)
+
+
+
+## Getting Started
+
+[Clerk](https://clerk.com/?utm_source=github&utm_medium=clerk_hono) is the easiest way to add authentication and user management to your Hono application. Add sign up, sign in, and profile management to your application in minutes.
+
+### Prerequisites
+
+- Hono 4+
+- Node.js 20+
+
+### Installation
+
+```sh
+npm install @clerk/hono
+```
+
+### Configuration
+
+Set your Clerk API keys as environment variables:
+
+```sh
+CLERK_SECRET_KEY=sk_****
+CLERK_PUBLISHABLE_KEY=pk_****
+```
+
+### Usage
+
+```typescript
+import { Hono } from 'hono';
+import { clerkMiddleware, getAuth } from '@clerk/hono';
+
+const app = new Hono();
+
+// Apply Clerk middleware to all routes
+app.use('*', clerkMiddleware());
+
+// Public route
+app.get('/', c => {
+ return c.json({ message: 'Hello!' });
+});
+
+// Protected route
+app.get('/protected', c => {
+ const { userId } = getAuth(c);
+
+ if (!userId) {
+ return c.json({ error: 'Unauthorized' }, 401);
+ }
+
+ return c.json({ message: 'Hello authenticated user!', userId });
+});
+
+export default app;
+```
+
+### Accessing the Clerk Client
+
+You can access the Clerk Backend API client directly from the context:
+
+```typescript
+app.get('/user/:id', async c => {
+ const clerkClient = c.get('clerk');
+ const user = await clerkClient.users.getUser(c.req.param('id'));
+ return c.json({ user });
+});
+```
+
+### Using `acceptsToken` for Machine Auth
+
+```typescript
+app.get('/api', c => {
+ const auth = getAuth(c, { acceptsToken: 'api_key' });
+
+ if (!auth.userId) {
+ return c.json({ error: 'Unauthorized' }, 401);
+ }
+
+ return c.json({ message: 'API access granted' });
+});
+```
+
+### Webhook Verification
+
+```typescript
+import { Hono } from 'hono';
+import { verifyWebhook } from '@clerk/hono/webhooks';
+
+const app = new Hono();
+
+app.post('/webhooks/clerk', async c => {
+ const evt = await verifyWebhook(c);
+
+ switch (evt.type) {
+ case 'user.created':
+ console.log('User created:', evt.data.id);
+ break;
+ // Handle other event types...
+ }
+
+ return c.json({ received: true });
+});
+```
+
+## Support
+
+You can get in touch with us in any of the following ways:
+
+- Join our official community [Discord server](https://clerk.com/discord)
+- On [our support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_hono)
+
+## Contributing
+
+We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md).
+
+## Security
+
+`@clerk/hono` follows good practices of security, but 100% security cannot be assured.
+
+`@clerk/hono` is provided **"as is"** without any **warranty**. Use at your own risk.
+
+_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._
+
+## License
+
+This project is licensed under the **MIT license**.
+
+See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/hono/LICENSE) for more information.
diff --git a/packages/hono/package.json b/packages/hono/package.json
new file mode 100644
index 00000000000..34f3b7c8db6
--- /dev/null
+++ b/packages/hono/package.json
@@ -0,0 +1,91 @@
+{
+ "name": "@clerk/hono",
+ "version": "0.0.1",
+ "description": "Clerk SDK for Hono",
+ "keywords": [
+ "auth",
+ "authentication",
+ "passwordless",
+ "session",
+ "jwt",
+ "hono",
+ "clerk"
+ ],
+ "homepage": "https://clerk.com/",
+ "bugs": {
+ "url": "https://github.com/clerk/javascript/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/clerk/javascript.git",
+ "directory": "packages/hono"
+ },
+ "license": "MIT",
+ "author": "Clerk",
+ "sideEffects": false,
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "require": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "./webhooks": {
+ "import": {
+ "types": "./dist/webhooks.d.mts",
+ "default": "./dist/webhooks.mjs"
+ },
+ "require": {
+ "types": "./dist/webhooks.d.ts",
+ "default": "./dist/webhooks.js"
+ }
+ },
+ "./types": {
+ "import": {
+ "types": "./dist/types.d.mts"
+ },
+ "require": {
+ "types": "./dist/types.d.ts"
+ }
+ }
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup --env.NODE_ENV production",
+ "clean": "rimraf ./dist",
+ "dev": "tsup --watch",
+ "format": "node ../../scripts/format-package.mjs",
+ "format:check": "node ../../scripts/format-package.mjs --check",
+ "lint": "eslint src",
+ "lint:attw": "attw --pack . --profile node16",
+ "lint:publint": "publint",
+ "publish:local": "pnpm yalc push --replace --sig",
+ "test": "vitest run",
+ "test:watch": "vitest watch"
+ },
+ "dependencies": {
+ "@clerk/backend": "workspace:^",
+ "@clerk/shared": "workspace:^"
+ },
+ "devDependencies": {
+ "hono": "^4.7.4"
+ },
+ "peerDependencies": {
+ "hono": ">=4"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/hono/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/hono/src/__tests__/__snapshots__/exports.test.ts.snap
new file mode 100644
index 00000000000..ed959cec5cf
--- /dev/null
+++ b/packages/hono/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -0,0 +1,8 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`@clerk/hono public exports > should not include a breaking change 1`] = `
+[
+ "clerkMiddleware",
+ "getAuth",
+]
+`;
diff --git a/packages/hono/src/__tests__/clerkMiddleware.test.ts b/packages/hono/src/__tests__/clerkMiddleware.test.ts
new file mode 100644
index 00000000000..154196c7b14
--- /dev/null
+++ b/packages/hono/src/__tests__/clerkMiddleware.test.ts
@@ -0,0 +1,258 @@
+import { Hono } from 'hono';
+
+import { clerkMiddleware, getAuth } from '../index';
+
+const EnvVariables = {
+ CLERK_SECRET_KEY: 'TEST_API_KEY',
+ CLERK_PUBLISHABLE_KEY: 'TEST_API_KEY',
+};
+
+const createMockSessionAuth = () => ({
+ tokenType: 'session_token' as const,
+ userId: 'user_123',
+ sessionId: 'sess_456',
+ orgId: null,
+ orgRole: null,
+ orgSlug: null,
+});
+
+const authenticateRequestMock = vi.fn();
+
+vi.mock(import('@clerk/backend'), async importOriginal => {
+ const original = await importOriginal();
+
+ return {
+ ...original,
+ createClerkClient(options: Parameters[0]) {
+ const client = original.createClerkClient(options);
+ vi.spyOn(client, 'authenticateRequest').mockImplementation(authenticateRequestMock);
+ return client;
+ },
+ };
+});
+
+describe('clerkMiddleware()', () => {
+ beforeEach(() => {
+ vi.stubEnv('CLERK_SECRET_KEY', EnvVariables.CLERK_SECRET_KEY);
+ vi.stubEnv('CLERK_PUBLISHABLE_KEY', EnvVariables.CLERK_PUBLISHABLE_KEY);
+ authenticateRequestMock.mockReset();
+ });
+
+ test('handles signin with Authorization Bearer', async () => {
+ authenticateRequestMock.mockResolvedValueOnce({
+ headers: new Headers(),
+ toAuth: createMockSessionAuth,
+ });
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+
+ app.get('/', c => {
+ const auth = getAuth(c);
+ return c.json({ auth });
+ });
+
+ const req = new Request('http://localhost/', {
+ headers: {
+ Authorization: 'Bearer deadbeef',
+ Origin: 'http://origin.com',
+ Host: 'host.com',
+ 'X-Forwarded-Port': '1234',
+ 'X-Forwarded-Host': 'forwarded-host.com',
+ Referer: 'referer.com',
+ 'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
+ },
+ });
+
+ const response = await app.request(req);
+
+ expect(response.status).toEqual(200);
+ expect(await response.json()).toEqual({ auth: createMockSessionAuth() });
+ expect(authenticateRequestMock).toHaveBeenCalledWith(
+ expect.any(Request),
+ expect.objectContaining({
+ secretKey: EnvVariables.CLERK_SECRET_KEY,
+ }),
+ );
+ });
+
+ test('handles signin with cookie', async () => {
+ authenticateRequestMock.mockResolvedValueOnce({
+ headers: new Headers(),
+ toAuth: createMockSessionAuth,
+ });
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+
+ app.get('/', c => {
+ const auth = getAuth(c);
+ return c.json({ auth });
+ });
+
+ const req = new Request('http://localhost/', {
+ headers: {
+ cookie: '_gcl_au=value1; ko_id=value2; __session=deadbeef; __client_uat=1675692233',
+ Origin: 'http://origin.com',
+ Host: 'host.com',
+ },
+ });
+
+ const response = await app.request(req);
+
+ expect(response.status).toEqual(200);
+ expect(await response.json()).toEqual({ auth: createMockSessionAuth() });
+ });
+
+ test('handles handshake by redirecting to FAPI', async () => {
+ authenticateRequestMock.mockResolvedValueOnce({
+ status: 'handshake',
+ reason: 'auth-reason',
+ message: 'auth-message',
+ headers: new Headers({
+ location: 'https://fapi.example.com/v1/clients/handshake',
+ 'x-clerk-auth-message': 'auth-message',
+ 'x-clerk-auth-reason': 'auth-reason',
+ 'x-clerk-auth-status': 'handshake',
+ }),
+ toAuth: createMockSessionAuth,
+ });
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+
+ app.get('/', c => {
+ const auth = getAuth(c);
+ return c.json({ auth });
+ });
+
+ const req = new Request('http://localhost/', {
+ headers: {
+ cookie: '__session=deadbeef; __client_uat=1675692233',
+ },
+ });
+
+ const response = await app.request(req);
+
+ expect(response.status).toEqual(307);
+ expect(Object.fromEntries(response.headers.entries())).toMatchObject({
+ location: 'https://fapi.example.com/v1/clients/handshake',
+ 'x-clerk-auth-status': 'handshake',
+ 'x-clerk-auth-reason': 'auth-reason',
+ 'x-clerk-auth-message': 'auth-message',
+ });
+ });
+
+ test('throws error when secret key is missing', async () => {
+ vi.stubEnv('CLERK_SECRET_KEY', '');
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+ app.get('/', c => c.json({ ok: true }));
+
+ const response = await app.request(new Request('http://localhost/'));
+
+ expect(response.status).toEqual(500);
+ });
+
+ test('throws error when publishable key is missing', async () => {
+ vi.stubEnv('CLERK_PUBLISHABLE_KEY', '');
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+ app.get('/', c => c.json({ ok: true }));
+
+ const response = await app.request(new Request('http://localhost/'));
+
+ expect(response.status).toEqual(500);
+ });
+});
+
+describe('getAuth()', () => {
+ beforeEach(() => {
+ vi.stubEnv('CLERK_SECRET_KEY', EnvVariables.CLERK_SECRET_KEY);
+ vi.stubEnv('CLERK_PUBLISHABLE_KEY', EnvVariables.CLERK_PUBLISHABLE_KEY);
+ authenticateRequestMock.mockReset();
+ });
+
+ test('throws error when called without middleware', async () => {
+ const app = new Hono();
+ app.get('/', c => {
+ const auth = getAuth(c);
+ return c.json({ auth });
+ });
+
+ const response = await app.request(new Request('http://localhost/'));
+
+ expect(response.status).toEqual(500);
+ });
+
+ test('handles acceptsToken option for API keys', async () => {
+ authenticateRequestMock.mockResolvedValueOnce({
+ headers: new Headers(),
+ toAuth: () => ({
+ tokenType: 'api_key',
+ id: 'ak_1234',
+ userId: 'user_456',
+ orgId: null,
+ }),
+ });
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+
+ app.get('/', c => {
+ const auth = getAuth(c, { acceptsToken: 'api_key' });
+ return c.json({ auth });
+ });
+
+ const req = new Request('http://localhost/', {
+ headers: {
+ Authorization: 'Bearer ak_deadbeef',
+ },
+ });
+
+ const response = await app.request(req);
+
+ expect(response.status).toEqual(200);
+ expect(await response.json()).toEqual({
+ auth: {
+ tokenType: 'api_key',
+ id: 'ak_1234',
+ userId: 'user_456',
+ orgId: null,
+ },
+ });
+ });
+
+ test('handles acceptsToken option as array', async () => {
+ authenticateRequestMock.mockResolvedValueOnce({
+ headers: new Headers(),
+ toAuth: () => ({
+ tokenType: 'api_key',
+ id: 'ak_5678',
+ userId: 'user_789',
+ orgId: null,
+ }),
+ });
+ const app = new Hono();
+ app.use('*', clerkMiddleware());
+
+ app.get('/', c => {
+ const auth = getAuth(c, { acceptsToken: ['api_key', 'session_token'] });
+ return c.json({ auth });
+ });
+
+ const req = new Request('http://localhost/', {
+ headers: {
+ Authorization: 'Bearer ak_deadbeef',
+ },
+ });
+
+ const response = await app.request(req);
+
+ expect(response.status).toEqual(200);
+ expect(await response.json()).toEqual({
+ auth: {
+ tokenType: 'api_key',
+ id: 'ak_5678',
+ userId: 'user_789',
+ orgId: null,
+ },
+ });
+ });
+});
diff --git a/packages/hono/src/__tests__/exports.test.ts b/packages/hono/src/__tests__/exports.test.ts
new file mode 100644
index 00000000000..1d2a129285b
--- /dev/null
+++ b/packages/hono/src/__tests__/exports.test.ts
@@ -0,0 +1,7 @@
+import * as publicExports from '../index';
+
+describe('@clerk/hono public exports', () => {
+ it('should not include a breaking change', () => {
+ expect(Object.keys(publicExports).sort()).toMatchSnapshot();
+ });
+});
diff --git a/packages/hono/src/clerkMiddleware.ts b/packages/hono/src/clerkMiddleware.ts
new file mode 100644
index 00000000000..81451046ba5
--- /dev/null
+++ b/packages/hono/src/clerkMiddleware.ts
@@ -0,0 +1,98 @@
+import type { AuthObject } from '@clerk/backend';
+import { createClerkClient } from '@clerk/backend';
+import type { AuthenticateRequestOptions, AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal';
+import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal';
+import type { MiddlewareHandler } from 'hono';
+import { env } from 'hono/adapter';
+
+type ClerkEnv = {
+ CLERK_SECRET_KEY: string;
+ CLERK_PUBLISHABLE_KEY: string;
+ CLERK_API_URL?: string;
+ CLERK_API_VERSION?: string;
+};
+
+export type ClerkMiddlewareOptions = Omit;
+
+/**
+ * Clerk middleware for Hono that authenticates requests and attaches
+ * auth data to the Hono context.
+ *
+ * @example
+ * ```ts
+ * import { Hono } from 'hono';
+ * import { clerkMiddleware, getAuth } from '@clerk/hono';
+ *
+ * const app = new Hono();
+ * app.use('*', clerkMiddleware());
+ *
+ * app.get('/', (c) => {
+ * const { userId } = getAuth(c);
+ * return c.json({ userId });
+ * });
+ * ```
+ */
+export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHandler => {
+ return async (c, next) => {
+ const clerkEnv = env(c);
+ const { secretKey, publishableKey, apiUrl, apiVersion, ...rest } = options || {
+ secretKey: clerkEnv.CLERK_SECRET_KEY || '',
+ publishableKey: clerkEnv.CLERK_PUBLISHABLE_KEY || '',
+ apiUrl: clerkEnv.CLERK_API_URL,
+ apiVersion: clerkEnv.CLERK_API_VERSION,
+ };
+
+ if (!secretKey) {
+ throw new Error(
+ 'Clerk: Missing Secret Key. Set CLERK_SECRET_KEY in your environment or pass secretKey to clerkMiddleware().',
+ );
+ }
+
+ if (!publishableKey) {
+ throw new Error(
+ 'Clerk: Missing Publishable Key. Set CLERK_PUBLISHABLE_KEY in your environment or pass publishableKey to clerkMiddleware().',
+ );
+ }
+
+ const clerkClient = createClerkClient({
+ ...rest,
+ apiUrl,
+ apiVersion,
+ secretKey,
+ publishableKey,
+ userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
+ });
+
+ const requestState = await clerkClient.authenticateRequest(c.req.raw, {
+ ...rest,
+ secretKey,
+ publishableKey,
+ acceptsToken: 'any',
+ });
+
+ if (requestState.headers) {
+ requestState.headers.forEach((value, key) => {
+ c.res.headers.append(key, value);
+ });
+
+ const locationHeader = requestState.headers.get('location');
+
+ if (locationHeader) {
+ return c.redirect(locationHeader, 307);
+ } else if (requestState.status === 'handshake') {
+ throw new Error('Clerk: Unexpected handshake without redirect');
+ }
+ }
+
+ const authObjectFn = ((authOptions?: AuthOptions) =>
+ getAuthObjectForAcceptedToken({
+ authObject: requestState.toAuth(authOptions) as AuthObject,
+ acceptsToken: 'any',
+ })) as GetAuthFnNoRequest;
+
+ c.set('clerkAuth', authObjectFn);
+ c.set('clerk', clerkClient);
+
+ await next();
+ };
+};
diff --git a/packages/hono/src/getAuth.ts b/packages/hono/src/getAuth.ts
new file mode 100644
index 00000000000..41d8f7c423a
--- /dev/null
+++ b/packages/hono/src/getAuth.ts
@@ -0,0 +1,37 @@
+import type { AuthOptions, GetAuthFn } from '@clerk/backend/internal';
+import type { Context } from 'hono';
+
+/**
+ * Retrieves the Clerk auth object from the Hono context.
+ * Must be used after clerkMiddleware() has been applied.
+ *
+ * @example
+ * ```ts
+ * app.get('/protected', (c) => {
+ * const { userId } = getAuth(c);
+ * if (!userId) {
+ * return c.json({ error: 'Unauthorized' }, 401);
+ * }
+ * return c.json({ message: 'Hello!' });
+ * });
+ * ```
+ *
+ * @example Using acceptsToken for API keys
+ * ```ts
+ * app.get('/api', (c) => {
+ * const auth = getAuth(c, { acceptsToken: 'api_key' });
+ * // auth will be typed for API key tokens
+ * });
+ * ```
+ */
+export const getAuth: GetAuthFn = ((c: Context, options?: AuthOptions) => {
+ const authFn = c.get('clerkAuth');
+
+ if (!authFn) {
+ throw new Error(
+ 'Clerk: getAuth() called without clerkMiddleware() being applied. Make sure to use clerkMiddleware() before calling getAuth().',
+ );
+ }
+
+ return authFn(options);
+}) as GetAuthFn;
diff --git a/packages/hono/src/global.d.ts b/packages/hono/src/global.d.ts
new file mode 100644
index 00000000000..1ae75219e34
--- /dev/null
+++ b/packages/hono/src/global.d.ts
@@ -0,0 +1,6 @@
+declare global {
+ const PACKAGE_NAME: string;
+ const PACKAGE_VERSION: string;
+}
+
+export {};
diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts
new file mode 100644
index 00000000000..948cce534bb
--- /dev/null
+++ b/packages/hono/src/index.ts
@@ -0,0 +1,14 @@
+export { clerkMiddleware } from './clerkMiddleware';
+export type { ClerkMiddlewareOptions } from './clerkMiddleware';
+
+export { getAuth } from './getAuth';
+
+import type { ClerkHonoVariables } from './types';
+export type { ClerkHonoVariables };
+
+// Augment Hono's ContextVariableMap so users get type inference
+// for c.get('clerk') and c.get('clerkAuth')
+declare module 'hono' {
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+ interface ContextVariableMap extends ClerkHonoVariables {}
+}
diff --git a/packages/hono/src/types.ts b/packages/hono/src/types.ts
new file mode 100644
index 00000000000..e7a97727ad8
--- /dev/null
+++ b/packages/hono/src/types.ts
@@ -0,0 +1,11 @@
+import type { ClerkClient } from '@clerk/backend';
+import type { GetAuthFnNoRequest } from '@clerk/backend/internal';
+
+/**
+ * Variables that clerkMiddleware sets on the Hono context.
+ * Access via c.get('clerk') and c.get('clerkAuth').
+ */
+export type ClerkHonoVariables = {
+ clerk: ClerkClient;
+ clerkAuth: GetAuthFnNoRequest;
+};
diff --git a/packages/hono/src/types/index.ts b/packages/hono/src/types/index.ts
new file mode 100644
index 00000000000..4a224c4726b
--- /dev/null
+++ b/packages/hono/src/types/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Re-export all shared Clerk types for convenient access via @clerk/hono/types
+ */
+export type * from '@clerk/shared/types';
+
+/**
+ * Hono-specific types
+ */
+export type { ClerkHonoVariables } from '../types';
diff --git a/packages/hono/src/webhooks.ts b/packages/hono/src/webhooks.ts
new file mode 100644
index 00000000000..408c47087cb
--- /dev/null
+++ b/packages/hono/src/webhooks.ts
@@ -0,0 +1,42 @@
+/* eslint-disable import/export */
+import type { VerifyWebhookOptions } from '@clerk/backend/webhooks';
+import { verifyWebhook as verifyWebhookBase } from '@clerk/backend/webhooks';
+import type { Context } from 'hono';
+
+// Re-export everything from backend webhooks
+export * from '@clerk/backend/webhooks';
+
+/**
+ * Verifies the authenticity of a webhook request from Clerk using Svix.
+ *
+ * @param c - The Hono Context object from the webhook handler
+ * @param options - Optional configuration object
+ * @param options.signingSecret - Custom signing secret. If not provided, falls back to CLERK_WEBHOOK_SIGNING_SECRET env variable
+ * @throws Will throw an error if the webhook signature verification fails
+ * @returns A promise that resolves to the verified webhook event data
+ *
+ * @example
+ * ```ts
+ * import { Hono } from 'hono';
+ * import { verifyWebhook } from '@clerk/hono/webhooks';
+ *
+ * const app = new Hono();
+ *
+ * app.post('/webhooks/clerk', async (c) => {
+ * const evt = await verifyWebhook(c);
+ * // Handle the webhook event
+ * return c.json({ received: true });
+ * });
+ * ```
+ *
+ * @see {@link https://clerk.com/docs/webhooks/sync-data} to learn more about syncing Clerk data to your application using webhooks
+ */
+export async function verifyWebhook(c: Context, options?: VerifyWebhookOptions) {
+ // Hono's c.req.raw is already a standard Web Request
+ // We need to clone it with the body for verification
+ const body = await c.req.text();
+ const clonedRequest = new Request(c.req.raw, {
+ body,
+ });
+ return verifyWebhookBase(clonedRequest, options);
+}
diff --git a/packages/hono/tsconfig.json b/packages/hono/tsconfig.json
new file mode 100644
index 00000000000..ffa09e4e241
--- /dev/null
+++ b/packages/hono/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "NodeNext",
+ "module": "NodeNext",
+ "sourceMap": false,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "allowJs": true,
+ "target": "ES2020",
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "resolveJsonModule": true
+ },
+ "include": ["src"]
+}
diff --git a/packages/hono/tsup.config.ts b/packages/hono/tsup.config.ts
new file mode 100644
index 00000000000..70495a48ec5
--- /dev/null
+++ b/packages/hono/tsup.config.ts
@@ -0,0 +1,26 @@
+import { defineConfig } from 'tsup';
+
+import { name, version } from './package.json';
+
+export default defineConfig(overrideOptions => {
+ const isWatch = !!overrideOptions.watch;
+
+ return {
+ entry: {
+ index: './src/index.ts',
+ webhooks: './src/webhooks.ts',
+ types: './src/types/index.ts',
+ },
+ format: ['cjs', 'esm'],
+ bundle: true,
+ clean: true,
+ minify: false,
+ sourcemap: true,
+ dts: true,
+ define: {
+ PACKAGE_NAME: `"${name}"`,
+ PACKAGE_VERSION: `"${version}"`,
+ __DEV__: `${isWatch}`,
+ },
+ };
+});
diff --git a/packages/hono/vitest.config.mts b/packages/hono/vitest.config.mts
new file mode 100644
index 00000000000..e7decfd80a1
--- /dev/null
+++ b/packages/hono/vitest.config.mts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ plugins: [],
+ test: {
+ globals: true,
+ coverage: {
+ provider: 'v8',
+ enabled: true,
+ reporter: ['text', 'json', 'html'],
+ },
+ env: {
+ CLERK_SECRET_KEY: 'TEST_SECRET_KEY',
+ CLERK_PUBLISHABLE_KEY: 'TEST_PUBLISHABLE_KEY',
+ },
+ setupFiles: './vitest.setup.mts',
+ },
+});
diff --git a/packages/hono/vitest.setup.mts b/packages/hono/vitest.setup.mts
new file mode 100644
index 00000000000..e8e72798208
--- /dev/null
+++ b/packages/hono/vitest.setup.mts
@@ -0,0 +1,6 @@
+import { beforeAll } from 'vitest';
+
+globalThis.PACKAGE_NAME = '@clerk/hono';
+globalThis.PACKAGE_VERSION = '0.0.0-test';
+
+beforeAll(() => {});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index adb035f97a6..72c2b119821 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -693,6 +693,19 @@ importers:
specifier: ^5.7.2
version: 5.7.2
+ packages/hono:
+ dependencies:
+ '@clerk/backend':
+ specifier: workspace:^
+ version: link:../backend
+ '@clerk/shared':
+ specifier: workspace:^
+ version: link:../shared
+ devDependencies:
+ hono:
+ specifier: ^4.7.4
+ version: 4.11.7
+
packages/localizations:
dependencies:
'@clerk/shared':
diff --git a/scripts/canary-core3.mjs b/scripts/canary-core3.mjs
index 0760d213ac3..66f067294cc 100755
--- a/scripts/canary-core3.mjs
+++ b/scripts/canary-core3.mjs
@@ -2,9 +2,10 @@
import { $, echo } from 'zx';
-import { constants, getPackageNames } from './common.mjs';
+import { constants, getChangesetIgnoredPackages, getPackageNames } from './common.mjs';
-const packageNames = await getPackageNames();
+const ignoredPackages = await getChangesetIgnoredPackages();
+const packageNames = (await getPackageNames()).filter(name => !ignoredPackages.has(name));
const packageEntries = packageNames.map(name => `'${name}': patch`).join('\n');
const snapshot = `---
diff --git a/scripts/canary.mjs b/scripts/canary.mjs
index 8e5e9c7beff..8aae064c236 100755
--- a/scripts/canary.mjs
+++ b/scripts/canary.mjs
@@ -2,9 +2,10 @@
import { $, echo } from 'zx';
-import { constants, getPackageNames, pinWorkspaceDeps } from './common.mjs';
+import { constants, getChangesetIgnoredPackages, getPackageNames, pinWorkspaceDeps } from './common.mjs';
-const packageNames = await getPackageNames();
+const ignoredPackages = await getChangesetIgnoredPackages();
+const packageNames = (await getPackageNames()).filter(name => !ignoredPackages.has(name));
const packageEntries = packageNames.map(name => `'${name}': patch`).join('\n');
const snapshot = `---
diff --git a/scripts/common.mjs b/scripts/common.mjs
index 44b9d2301c3..7fc8c33147b 100644
--- a/scripts/common.mjs
+++ b/scripts/common.mjs
@@ -5,6 +5,11 @@ export const constants = {
ChangesetConfigFile: '.changeset/config.json',
};
+export async function getChangesetIgnoredPackages() {
+ const config = JSON.parse(await readFile(constants.ChangesetConfigFile, 'utf-8'));
+ return new Set(config.ignore || []);
+}
+
export async function getPackageJsonFiles() {
const result = await $`find packages -mindepth 2 -maxdepth 2 -name package.json`.quiet();
return result.stdout.trim().split('\n').filter(Boolean);
diff --git a/scripts/snapshot.mjs b/scripts/snapshot.mjs
index 43e4ce90f71..a9c7e2b7191 100755
--- a/scripts/snapshot.mjs
+++ b/scripts/snapshot.mjs
@@ -2,9 +2,10 @@
import { $, argv, echo } from 'zx';
-import { constants, getPackageNames, pinWorkspaceDeps } from './common.mjs';
+import { constants, getChangesetIgnoredPackages, getPackageNames, pinWorkspaceDeps } from './common.mjs';
-const packageNames = await getPackageNames();
+const ignoredPackages = await getChangesetIgnoredPackages();
+const packageNames = (await getPackageNames()).filter(name => !ignoredPackages.has(name));
const packageEntries = packageNames.map(name => `'${name}': patch`).join('\n');
const snapshot = `---