velvet-auth
velvet-auth is a production-ready authentication plugin specifically designed for Elysia.js applications running on Bun. It provides a comprehensive solution for common authentication patterns, including JWT rotation, secure password hashing using native Argon2id (via `Bun.password`), and session management with RESP-compatible stores like Redis for refresh token invalidation and JTI blacklisting. The current stable version is 0.1.9, with frequent minor releases addressing bug fixes and introducing improvements. Key differentiators include its tight integration with Bun's native features, an adapter pattern for database and email provider flexibility, and a focus on type safety with Zod validation. It aims to reduce boilerplate for setting up robust auth stacks in the Bun/Elysia ecosystem.
Common errors
-
Error: The provided secret must be at least 32 characters long.
cause The `jwt.secret` configuration option is too short, leading to an insecure JWT setup.fixUpdate your `velvetAuth` configuration to provide a `jwt.secret` that is at least 32 characters long. Example: `{ jwt: { secret: process.env.JWT_SECRET || 'your-long-and-secure-secret-key-here' } }` -
TypeError: Bun.password is not a function
cause The application is likely being run in a Node.js environment instead of Bun, or Bun is an outdated version.fixEnsure your project is executed using Bun (e.g., `bun run start`) and that your Bun version is >= 1.0. -
Error: Redis connection failed: connect ECONNREFUSED 127.0.0.1:6379
cause The application cannot establish a connection to the Redis server, which might be offline, misconfigured, or running on a different port/host.fixVerify that your Redis server is running and accessible at the configured `redis.url` (default `redis://localhost:6379`). Check firewall rules and Redis server status. You can configure the URL via `velvetAuth({ redis: { url: 'your_redis_url' } })` or `process.env.REDIS_URL`. -
Property 'user' does not exist on type 'Context'.
cause The `ctx.user` object is not correctly typed or available in the handler, likely because the `createAuthGuard` was not applied, or applied incorrectly to the route.fixEnsure you are using `createAuthGuard()` on the routes where `ctx.user` is expected. Example: `app.group('/protected', (app) => app.use(createAuthGuard()).get('/', (ctx) => ctx.user))`.
Warnings
- gotcha The `createAuthGuard` function in v0.1.9 changed its internal `.derive()` call from `{ as: "global" }` to `{ as: "scoped" }`. This change prevents the user context from unintentionally leaking into unrelated routes when the guard is composed into a larger Elysia app. While a fix for context isolation, it might affect assumptions in existing applications that relied on the global derivation behavior.
- gotcha The `jwt.secret` configuration option is critical for security and must be a string with a minimum length of 32 characters. Using a shorter or easily guessable secret makes JWTs vulnerable to brute-force attacks.
- breaking Earlier versions (before v0.1.8) might have silently ignored password policy rules defined in `config.password` (e.g., `minLength`, `requireUppercase`). Since v0.1.8, these rules are actively enforced during registration and will return a `400` error if violated.
- gotcha velvet-auth relies heavily on Bun's native features, specifically `Bun.password` for Argon2id hashing and `Bun.RedisClient`. This means it is strictly a Bun-only package and will not work with Node.js or Deno runtimes.
Install
-
npm install velvet-auth -
yarn add velvet-auth -
pnpm add velvet-auth
Imports
- velvetAuth
const velvetAuth = require('velvet-auth')import { velvetAuth } from 'velvet-auth' - createAuthGuard
import createAuthGuard from 'velvet-auth/guard'
import { createAuthGuard } from 'velvet-auth' - UserStoreAdapter
import { UserStoreAdapter } from 'velvet-auth'import type { UserStoreAdapter } from 'velvet-auth'
Quickstart
import { Elysia } from "elysia";
import { velvetAuth } from "velvet-auth";
import { BunFileRouter } from 'bun-file-router'; // Assuming a simple db mock for demonstration
const db = {
users: {
data: [] as any[], // In-memory mock for demonstration
findOne: async ({ id, username, email }: { id?: string; username?: string; email?: string }) => {
if (id) return db.users.data.find(u => u.id === id);
if (username) return db.users.data.find(u => u.username === username);
if (email) return db.users.data.find(u => u.email === email);
return undefined;
},
insert: async (data: any) => { db.users.data.push({ ...data, id: Date.now().toString() }); return data; },
update: async ({ id }: { id: string }, updates: any) => {
const userIndex = db.users.data.findIndex(u => u.id === id);
if (userIndex !== -1) {
db.users.data[userIndex] = { ...db.users.data[userIndex], ...updates };
}
},
},
};
// 1. Implement the UserStoreAdapter for your database
const userStore = {
findById: async (id) => db.users.findOne({ id }),
findByUsername: async (username) => db.users.findOne({ username }),
findByEmail: async (email) => db.users.findOne({ email }),
create: async (data) => db.users.insert(data),
updatePassword: async (id, hash) => db.users.update({ id }, { password: hash }),
setEmailVerified: async (id) => db.users.update({ id }, { emailVerified: true }),
};
// 2. Implement the EmailAdapter for your email provider
const emailAdapter = {
sendOtp: async (to, otp) => { console.log(`Sending OTP to ${to}: ${otp}`); return true; },
sendVerification: async (to, url) => { console.log(`Sending verification to ${to}: ${url}`); return true; },
checkStatus: async () => true,
};
// 3. Mount the plugin
const app = new Elysia()
.use(
velvetAuth(userStore, emailAdapter, {
jwt: {
secret: process.env.JWT_SECRET ?? 'super-secret-jwt-key-that-is-at-least-32-chars-long',
},
redis: {
url: process.env.REDIS_URL ?? 'redis://localhost:6379'
},
password: {
minLength: 8, // Example: enforce min length
}
}),
)
.get('/', () => 'Welcome to velvet-auth example!')
.listen(3000);
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);