Authentication Guide
Complete guide to implementing authentication in ObjectStack using plugin-auth with Better-Auth
Authentication Guide
Complete guide to implementing authentication in ObjectStack applications using the @objectstack/plugin-auth package powered by Better-Auth.
Table of Contents
- Overview
- Installation
- Basic Setup
- Authentication Methods
- OAuth Providers
- Advanced Features
- Client Integration
- API Reference
- Best Practices
- MSW/Mock Mode
Overview
The @objectstack/plugin-auth package provides enterprise-grade authentication and identity management for ObjectStack applications. It's built on top of Better-Auth, a modern authentication library, and seamlessly integrates with ObjectStack's kernel architecture.
Key Features
- ✅ Email/Password Authentication - Traditional username/password login
- ✅ OAuth Providers - Google, GitHub, and more
- ✅ Session Management - Automatic session handling with configurable expiry
- ✅ Password Reset - Email-based password reset flow
- ✅ Email Verification - Email verification workflow
- ✅ 2FA - Two-factor authentication
- ✅ Passkeys - WebAuthn/Passkey support
- ✅ Magic Links - Passwordless authentication
- ✅ Organizations - Multi-tenant support
- ✅ ObjectQL Integration - Native ObjectStack data persistence (no ORM required)
Architecture
The plugin uses a direct forwarding architecture where all authentication requests are forwarded to Better-Auth's universal handler. This ensures:
- Full compatibility with all Better-Auth features
- Minimal custom code to maintain
- Easy updates when Better-Auth releases new features
- Type-safe API access via
authManager.api
Installation
Install the plugin in your ObjectStack project:
pnpm add @objectstack/plugin-authThe plugin requires Better-Auth as a peer dependency, which will be automatically installed.
Basic Setup
1. Environment Variables
Set up your authentication secret in .env:
# Required: Secret for session token encryption
AUTH_SECRET=your-super-secret-key-min-32-chars
# Optional: OAuth providers (if using)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secretImportant: Never commit
AUTH_SECRETto version control. Use a strong random string (minimum 32 characters).
2. Plugin Configuration
Add the plugin to your kernel configuration:
import { ObjectKernel } from '@objectstack/core';
import { AuthPlugin } from '@objectstack/plugin-auth';
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
const kernel = new ObjectKernel({
plugins: [
// HTTP server (optional — auth works without it in MSW/mock mode)
new HonoServerPlugin({
port: 3000,
}),
// Authentication plugin
new AuthPlugin({
secret: process.env.AUTH_SECRET,
baseUrl: 'http://localhost:3000',
}),
]
});
await kernel.start();That's it! Your authentication endpoints are now available at /api/v1/auth/*.
MSW/Mock Mode: AuthPlugin does not require an HTTP server. When HonoServerPlugin is absent, the plugin gracefully skips route registration and still registers the
authservice. See MSW/Mock Mode below for details.
3. ObjectQL Data Persistence
The plugin automatically uses ObjectQL for data persistence. No additional database configuration is required - it works with your existing ObjectQL setup.
The plugin creates the following auth objects (using ObjectStack sys_ protocol names):
sys_user- User accounts (mapped from better-auth'susermodel)sys_session- Active sessions (mapped from better-auth'ssessionmodel)sys_account- OAuth provider accounts (mapped from better-auth'saccountmodel)sys_verification- Email/phone verification tokens (mapped from better-auth'sverificationmodel)
Note: better-auth uses hardcoded model names (
user,session, etc.). The ObjectQL adapter automatically maps these tosys_-prefixed protocol names viaAUTH_MODEL_TO_PROTOCOL. Client-side API routes (/api/v1/auth/*) are not affected — they do not expose object names.
Authentication Methods
Email/Password Authentication
Sign Up
// Client-side (using @objectstack/client)
import { ObjectStackClient } from '@objectstack/client';
const client = new ObjectStackClient({
baseUrl: 'http://localhost:3000'
});
const result = await client.auth.register({
email: 'user@example.com',
password: 'securePassword123',
name: 'John Doe'
});
console.log('User created:', result.data.user);
console.log('Access token:', result.data.token);Sign In
const session = await client.auth.login({
type: 'email',
email: 'user@example.com',
password: 'securePassword123'
});
console.log('Logged in:', session.data.user);Sign Out
await client.auth.logout();Get Current Session
const session = await client.auth.me();
console.log('Current user:', session.data.user);Password Management
Request Password Reset
// Direct API call
const response = await fetch('http://localhost:3000/api/v1/auth/forget-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com'
})
});Reset Password with Token
const response = await fetch('http://localhost:3000/api/v1/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: 'reset-token-from-email',
password: 'newSecurePassword456'
})
});Email Verification
Send Verification Email
const response = await fetch('http://localhost:3000/api/v1/auth/send-verification-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
});Verify Email
// User clicks link in email with token
const response = await fetch(`http://localhost:3000/api/v1/auth/verify-email?token=${token}`);OAuth Providers
Configuration
Enable OAuth providers in your plugin configuration:
new AuthPlugin({
secret: process.env.AUTH_SECRET,
baseUrl: 'http://localhost:3000',
providers: [
{
id: 'google',
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
{
id: 'github',
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}
]
})OAuth Flow
1. Initiate OAuth Login
// Redirect user to OAuth provider
window.location.href = 'http://localhost:3000/api/v1/auth/authorize/google';2. Handle Callback
Better-Auth automatically handles the OAuth callback at /api/v1/auth/callback/google and redirects the user back to your application with a session.
Advanced Features
Two-Factor Authentication (2FA)
Enable 2FA in your configuration:
new AuthPlugin({
secret: process.env.AUTH_SECRET,
baseUrl: 'http://localhost:3000',
plugins: {
twoFactor: true, // Enable 2FA
}
})Enable 2FA for User
const response = await fetch('http://localhost:3000/api/v1/auth/two-factor/enable', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
});
const { qrCode, secret } = await response.json();
// Display qrCode to user for scanning with authenticator appVerify 2FA Code
const response = await fetch('http://localhost:3000/api/v1/auth/two-factor/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: '123456' // Code from authenticator app
})
});Passkeys (WebAuthn)
Enable passkey support:
new AuthPlugin({
secret: process.env.AUTH_SECRET,
baseUrl: 'http://localhost:3000',
plugins: {
passkeys: true, // Enable passkey support
}
})Register a Passkey
const response = await fetch('http://localhost:3000/api/v1/auth/passkey/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
});Authenticate with Passkey
const response = await fetch('http://localhost:3000/api/v1/auth/passkey/authenticate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});Magic Links
Enable passwordless magic link authentication:
new AuthPlugin({
secret: process.env.AUTH_SECRET,
baseUrl: 'http://localhost:3000',
plugins: {
magicLink: true, // Enable magic links
}
})Send Magic Link
const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com'
})
});Verify Magic Link
// User clicks link in email
const response = await fetch(`http://localhost:3000/api/v1/auth/magic-link/verify?token=${token}`);Organizations (Multi-Tenant)
Enable organization/team support:
new AuthPlugin({
secret: process.env.AUTH_SECRET,
baseUrl: 'http://localhost:3000',
plugins: {
organization: true, // Enable organizations
}
})Client Integration
Using @objectstack/client
The official ObjectStack client has built-in auth methods:
import { ObjectStackClient } from '@objectstack/client';
const client = new ObjectStackClient({
baseUrl: 'http://localhost:3000'
});
// Register
await client.auth.register({
email: 'user@example.com',
password: 'password123',
name: 'John Doe'
});
// Login (auto-sets token)
await client.auth.login({
type: 'email',
email: 'user@example.com',
password: 'password123'
});
// Now all subsequent requests include the auth token
const tasks = await client.data.find('task', {});
// Logout (clears token)
await client.auth.logout();
// Get current user
const session = await client.auth.me();
// Refresh token
await client.auth.refreshToken('refresh-token-value');Direct API Calls
All endpoints are available at /api/v1/auth/*:
// Example: Login
const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password123'
})
});
const data = await response.json();
const token = data.data.token;
// Use token in subsequent requests
const protectedResponse = await fetch('http://localhost:3000/api/v1/data/task', {
headers: {
'Authorization': `Bearer ${token}`
}
});API Reference
Authentication Endpoints
All endpoints are available under /api/v1/auth/*:
Email/Password
POST /api/v1/auth/sign-in/email- Sign in with email and passwordPOST /api/v1/auth/sign-up/email- Register new userPOST /api/v1/auth/sign-out- Sign out current user
Session
GET /api/v1/auth/get-session- Get current user session
Password Management
POST /api/v1/auth/forget-password- Request password reset emailPOST /api/v1/auth/reset-password- Reset password with token
Email Verification
POST /api/v1/auth/send-verification-email- Send verification emailGET /api/v1/auth/verify-email- Verify email with token
OAuth
GET /api/v1/auth/authorize/[provider]- Start OAuth flowGET /api/v1/auth/callback/[provider]- OAuth callback handler
Two-Factor Authentication
POST /api/v1/auth/two-factor/enable- Enable 2FA for userPOST /api/v1/auth/two-factor/verify- Verify 2FA code
Passkeys
POST /api/v1/auth/passkey/register- Register a passkeyPOST /api/v1/auth/passkey/authenticate- Authenticate with passkey
Magic Links
POST /api/v1/auth/magic-link/send- Send magic link emailGET /api/v1/auth/magic-link/verify- Verify magic link
For complete API documentation, see the Better-Auth API Reference.
Best Practices
Security
-
Use Strong Secrets: Generate a strong random secret for
AUTH_SECRET(minimum 32 characters)# Generate a secure secret openssl rand -base64 32 -
HTTPS in Production: Always use HTTPS for authentication endpoints in production
new AuthPlugin({ baseUrl: process.env.NODE_ENV === 'production' ? 'https://api.example.com' : 'http://localhost:3000', }) -
Environment Variables: Never commit secrets to version control
# .env (not committed) AUTH_SECRET=your-secret-here GOOGLE_CLIENT_ID=your-client-id -
Session Expiry: Configure appropriate session expiry times
new AuthPlugin({ secret: process.env.AUTH_SECRET, baseUrl: 'http://localhost:3000', session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // Update every 24 hours } })
User Experience
- Email Verification: Require email verification for sensitive operations
- Password Requirements: Enforce strong password policies
- 2FA for Admin: Require 2FA for administrative accounts
- OAuth Options: Provide multiple OAuth providers for convenience
Error Handling
try {
await client.auth.login({
type: 'email',
email: 'user@example.com',
password: 'wrong-password'
});
} catch (error) {
if (error.response?.status === 401) {
console.error('Invalid credentials');
} else {
console.error('Login failed:', error.message);
}
}ObjectStack Field Naming
The plugin uses ObjectStack's sys_ prefix convention for protocol object names and snake_case for field names, which is required by the ObjectStack protocol:
- Object names:
sys_user,sys_session,sys_account,sys_verification(protocol names) - Field names:
email_verified,created_at,user_id(snake_case)
better-auth internally uses camelCase model and field names (user, emailVerified, userId).
The plugin bridges this gap using better-auth's official modelName / fields schema customisation API:
// Declared in the betterAuth() config via AUTH_*_CONFIG constants:
user: { modelName: 'sys_user', fields: { emailVerified: 'email_verified', … } },
session: { modelName: 'sys_session', fields: { userId: 'user_id', expiresAt: 'expires_at', … } },
account: { modelName: 'sys_account', fields: { providerId: 'provider_id', accountId: 'account_id', … } },
verification: { modelName: 'sys_verification', fields: { expiresAt: 'expires_at', … } },The ObjectQL adapter factory (createObjectQLAdapterFactory) then uses better-auth's createAdapterFactory
which automatically transforms all data and where-clauses using these mappings — no manual
camelCase ↔ snake_case conversion is needed in the adapter.
Upgrade note: If you have custom adapters or plugins that reference auth objects by name, update them to use
sys_user,sys_session,sys_account,sys_verification(or import fromSystemObjectNameconstants).
MSW/Mock Mode
AuthPlugin is designed to work in both server and MSW/mock (browser-only) environments. This means you can develop and test authentication flows without running a real HTTP server.
How It Works
- Server mode (HonoServerPlugin active): AuthPlugin registers HTTP routes at
/api/v1/auth/*and forwards all requests to better-auth. - MSW/mock mode (no HTTP server): AuthPlugin gracefully skips route registration but still registers the
authservice. TheHttpDispatcherprovides mock fallback responses for core auth endpoints.
Minimal Configuration for Mock Mode
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { AuthPlugin } from '@objectstack/plugin-auth';
const kernel = new ObjectKernel();
await kernel.use(new ObjectQLPlugin());
await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory'));
// AuthPlugin works without HonoServerPlugin — no HTTP server needed
await kernel.use(new AuthPlugin({
secret: 'INSECURE_DEV_ONLY_mock_secret_do_not_use_in_production',
baseUrl: 'http://localhost:5173',
}));
await kernel.bootstrap();⚠️ Warning: The secret above is for local development only. In production, always use a strong random secret from an environment variable (
process.env.AUTH_SECRET).
Mock Fallback Endpoints
When no auth service handler is registered and the legacy broker login is unavailable, HttpDispatcher.handleAuth() automatically provides mock responses for:
| Endpoint | Method | Description |
|---|---|---|
sign-up/email | POST | Returns mock user + session |
sign-in/email | POST | Returns mock user + session |
login | POST | Legacy login — returns mock user + session |
register | POST | Alias for sign-up |
get-session | GET | Returns { session: null, user: null } |
sign-out | POST | Returns { success: true } |
This ensures that registration and sign-in flows do not return 404 errors in MSW/browser-only environments.
Note: In server mode with AuthPlugin loaded, the auth service handler takes priority and the mock fallback is never reached. The mock fallback only activates when AuthPlugin is not loaded (e.g. browser-only Studio builds where
better-authis unavailable).
Studio Kernel Factory
The Studio app runs in the browser, where the Node-only better-auth library cannot be bundled. Instead of loading AuthPlugin directly, Studio relies on HttpDispatcher's built-in mock fallback to handle auth endpoints in MSW mode:
// apps/studio/src/mocks/createKernel.ts
// No AuthPlugin needed — HttpDispatcher provides mock auth endpoints automatically
const kernel = new ObjectKernel();
await kernel.use(new ObjectQLPlugin());
await kernel.use(new DriverPlugin(driver, 'memory'));
// ...
await kernel.use(new MSWPlugin({ /* ... */ }));
await kernel.bootstrap();Next Steps
- See Security Guide for authorization and permissions
- See Client SDK Guide for client-side integration
- See API Reference for complete API documentation
- Visit Better-Auth Documentation for advanced features
Examples
Complete working examples are available in the repository: