npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

carnojs-better-auth

v0.1.4

Published

Better Auth integration plugin for Carno.js applications

Readme

carnojs-better-auth

Better Auth integration for Carno.js applications.

Mounts Better Auth HTTP routes on your Carno app, exposes a DI-friendly BetterAuthService, and ships middleware for protecting controllers with session cookies.

Requirements

  • Bun >= 1.0
  • @carno.js/core >= 1.0
  • better-auth >= 1.0

Install

bun add carnojs-better-auth @carno.js/core better-auth

Quick start

import { Carno } from '@carno.js/core';
import { CarnoBetterAuth } from 'carnojs-better-auth';
import { memoryAdapter } from 'better-auth/adapters/memory';

const app = new Carno();

app.use(
    CarnoBetterAuth({
        baseURL: 'http://localhost:3000',
        database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),
        emailAndPassword: { enabled: true },
    }),
);

app.listen(3000);

Register one CarnoBetterAuth() plugin per Carno application. A second app.use(CarnoBetterAuth(...)) throws immediately — duplicate registration would merge conflicting auth controllers and DI providers.

Better Auth routes are mounted at /auth by default (for example /auth/sign-up/email, /auth/get-session). Better Auth's own docs often use /api/auth; this plugin defaults to /auth because Carno apps typically prefix API controllers explicitly (for example @Controller("/api/me")) rather than nesting entire sub-apps. Set basePath: "/api/auth" if you want to match the Better Auth guides exactly.

Server baseURL should be your app origin (for example http://localhost:3000). Client baseURL must include the auth path:

import { createAuthClient } from 'better-auth/client';
import { buildAuthClientBaseURL, DEFAULT_AUTH_BASE_PATH } from 'carnojs-better-auth';

export const authClient = createAuthClient({
    baseURL: buildAuthClientBaseURL('http://localhost:3000', DEFAULT_AUTH_BASE_PATH),
    // equivalent to: baseURL: "http://localhost:3000/auth"
});

If baseURL includes a path that does not match basePath (for example http://localhost:3000/api/auth with default basePath: "/auth"), the plugin logs a startup warning with the corrected client URL.

Set baseURL in your Better Auth server options (or the BETTER_AUTH_URL env var). Better Auth uses it for callbacks, redirects, and cookie handling.

Cross-origin requests (CORS)

Better Auth routes are registered as a Carno controller, so they inherit CORS from the host app. Configure CORS once on your main Carno instance:

const cors = {
    origins: 'http://localhost:5173',
    credentials: true,
};

const app = new Carno({ cors });

app.use(
    CarnoBetterAuth({
        baseURL: 'http://localhost:3000',
        trustedOrigins: ['http://localhost:5173'],
        // ...
    }),
);
  • cors (on Carno) — Carno CORS config; applies to /auth/* and all other controller routes. Unmatched OPTIONS preflight requests receive Carno's standard preflight response.
  • trustedOrigins — Better Auth's own origin allowlist for cookies and CSRF (cookies guide). Configure both for cross-origin SPAs.

Middleware and auth routes

Auth routes run through Carno's controller pipeline, so host middleware applies automatically:

const app = new Carno({
    cors,
    globalMiddlewares: [LoggingMiddleware],
});

app.middlewares([RequestIdMiddleware]);

app.use(CarnoBetterAuth({ baseURL: 'http://localhost:3000' /* ... */ }));

| Traffic | Carno middleware (globalMiddlewares, app.middlewares(), @Middleware) | BetterAuthMiddleware | | ------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------ | | /auth/* | Yes | No (unless applied to the auth controller) | | Protected @Controller routes | When applied | Only when applied |

For advanced cases where you must intercept the raw Better Auth Request → Response handler before Carno's Context layer, use wrapHandler:

app.use(
    CarnoBetterAuth({
        baseURL: 'http://localhost:3000',
        wrapHandler: (handler) => async (req) => {
            const response = await handler(req);
            const headers = new Headers(response.headers);
            headers.set('x-auth-pipeline', '1');
            return new Response(response.body, {
                status: response.status,
                statusText: response.statusText,
                headers,
            });
        },
        // ...
    }),
);

Prefer Carno middleware for logging, rate limits, and request IDs — it runs on /auth/* without extra configuration.

Protecting routes

Apply BetterAuthMiddleware to controllers that require a session. Authenticated user and session are exposed via @Locals:

import { Controller, Get, Locals, Middleware } from '@carno.js/core';
import { AUTH_SESSION_KEY, AUTH_USER_KEY, BetterAuthMiddleware } from 'carnojs-better-auth';

@Controller('/me')
@Middleware(BetterAuthMiddleware)
class MeController {
    @Get()
    me(
        @Locals(AUTH_USER_KEY) user: { id: string; email: string },
        @Locals(AUTH_SESSION_KEY) session: { userId: string },
    ) {
        return { email: user.email, sessionUserId: session.userId };
    }
}

Unauthenticated requests receive 401 with Better Auth's standard JSON error shape:

{ "code": "UNAUTHORIZED", "message": "Unauthorized" }

When session lookup fails unexpectedly (for example a database outage), protected routes return 503 with Carno's standard { "statusCode": 503, "message": "Authentication service unavailable" } payload instead of an unhandled 500.

Programmatic access

Inject BetterAuthService anywhere Carno DI resolves services:

import { Service } from '@carno.js/core';
import { BetterAuthService } from 'carnojs-better-auth';

@Service()
class AccountService {
    constructor(private readonly auth: BetterAuthService) {}

    async getSession(headers: Headers) {
        return this.auth.auth.api.getSession({ headers });
    }
}

Custom auth path

Pass basePath in the plugin options to change the mount point:

CarnoBetterAuth({
    basePath: '/api/auth',
    baseURL: 'http://localhost:3000',
    // ...
});

Keep basePath aligned with Better Auth's own basePath option so route registration and the auth handler agree.

Use a dedicated path segment such as /auth or /api/auth. The value / (or an empty string) is rejected because it would register a catch-all /* route and hijack your application.

App-level URL prefix

Carno does not support app.use("/api", subApp) path-prefix mounting. Prefix URLs on each controller and align Better Auth with the same basePath:

import { Carno, Controller, Get, Middleware } from '@carno.js/core';
import { CarnoBetterAuth, BetterAuthMiddleware } from 'carnojs-better-auth';

@Controller('/api/me')
@Middleware(BetterAuthMiddleware)
class ApiMeController {
    @Get()
    me() {
        return { ok: true };
    }
}

const app = new Carno();

app.use(
    CarnoBetterAuth({
        basePath: '/api/auth',
        baseURL: 'http://localhost:3000',
        // ...
    }),
);

app.controllers([ApiMeController]);
app.listen(3000);
// Auth:     http://localhost:3000/api/auth/*
// API:      http://localhost:3000/api/me

For nested controllers, Carno also supports @Controller({ path: "/api", children: [ApiMeController] }) so child routes inherit the parent path. See the Carno.js repository for controller nesting details.

When using a custom auth path, set the Better Auth client baseURL to include it (for example http://localhost:3000/api/auth).

Public API

| Export | Description | | ---------------------------------------------- | ------------------------------------------------------------------- | | CarnoBetterAuth(options?) | Carno plugin factory | | BetterAuthService | Injectable wrapper around the Better Auth instance | | BetterAuthMiddleware | Session guard; populates ctx.locals | | BetterAuthConfig | Internal config token (advanced/testing) | | AUTH_USER_KEY / AUTH_SESSION_KEY | Locals keys for @Locals() | | DEFAULT_AUTH_BASE_PATH | Default mount path (/auth) | | buildAuthClientBaseURL(origin, basePath) | Client SDK base URL helper | | BetterAuthModuleOptions | Alias of Better Auth's BetterAuthOptions | | CarnoBetterAuthOptions | Plugin options (BetterAuthModuleOptions + optional wrapHandler) | | AuthRouteHandler / AuthRouteHandlerWrapper | Types for wrapHandler | | AuthContext / AuthLocals | Session typing helpers |

Development

bun install          # installs Husky hooks via prepare
bun run format       # Prettier write
bun run format:check # Prettier CI check
bun run fallow       # dead code / dupes / health report
bun run fallow:fix   # apply auto-fixes
bun run typecheck    # src + test projects
bun run test:unit    # tests only (no rebuild)
bun run test         # build + full test suite
bun run build        # tsdown → dist/

Pre-commit runs Prettier on staged files and applies Fallow fixes when TypeScript files are staged. Pre-push runs the full test suite.

License

Apache-2.0