Cookbook
Authentication
Signup, login, logout, API key auth, and role-based access control patterns
Common authentication patterns for Arcway apps.
Signup with Password Hashing
// api/auth/signup.js
import { vault } from 'arcway';
export const POST = {
schema: {
body: {
name: 'string>0',
email: 'string.email',
password: 'string>=8',
},
},
handler: async (ctx) => {
const { db, events } = ctx;
const { name, email, password } = ctx.req.body;
const existing = await db('users').where('email', email).first();
if (existing) {
return { status: 409, error: { code: 'EMAIL_TAKEN', message: 'Email already registered' } };
}
const hash = await vault.hashPassword(password);
const [id] = await db('users').insert({
id: vault.generateNanoId(),
name,
email,
password_hash: hash,
});
const user = await db('users').where('id', id).select('id', 'name', 'email').first();
await events.emit('users/created', { id: user.id, email: user.email });
return {
status: 201,
data: user,
session: { userId: user.id },
};
},
};Login
// api/auth/login.js
import { vault } from 'arcway';
export const POST = {
schema: {
body: { email: 'string.email', password: 'string>0' },
},
handler: async (ctx) => {
const { db } = ctx;
const { email, password } = ctx.req.body;
const user = await db('users').where('email', email).first();
if (!user || !(await vault.verifyPassword(password, user.password_hash))) {
return { status: 401, error: { code: 'INVALID_CREDENTIALS', message: 'Wrong email or password' } };
}
return {
data: { id: user.id, name: user.name, email: user.email },
session: { userId: user.id, role: user.role },
};
},
};Logout
// api/auth/logout.js
export const POST = {
handler: async (ctx) => {
return { data: { ok: true }, session: null };
},
};Current User Endpoint
// api/auth/me.js
export const GET = {
handler: async (ctx) => {
const { db } = ctx;
if (!ctx.req.session?.userId) {
return { status: 401, error: { code: 'UNAUTHORIZED' } };
}
const user = await db('users')
.where('id', ctx.req.session.userId)
.select('id', 'name', 'email', 'role')
.first();
if (!user) {
return { status: 401, error: { code: 'UNAUTHORIZED' }, session: null };
}
return { data: user };
},
};Protected Routes with Middleware
Use global middleware to require authentication on all routes, allowing auth endpoints through:
// api/_middleware.js
export default {
handler: async (ctx) => {
// Allow auth routes through
if (ctx.req.path.startsWith('/api/auth/')) return;
if (!ctx.req.session?.userId) {
return { status: 401, error: { code: 'UNAUTHORIZED', message: 'Login required' } };
}
},
};API Key Authentication
For machine-to-machine access, validate an API key header using method-specific middleware:
// api/webhooks/_middleware.js
// Only apply auth to non-GET requests
export const POST = {
handler: async (ctx) => {
const { db, log } = ctx;
const key = ctx.req.headers['x-api-key'];
if (!key) {
return { status: 401, error: { code: 'MISSING_API_KEY', message: 'API key required' } };
}
const apiKey = await db('api_keys').where('key', key).where('active', true).first();
if (!apiKey) {
return { status: 401, error: { code: 'INVALID_API_KEY', message: 'Invalid or revoked API key' } };
}
// Log API key usage
log.info('API key used', { keyId: apiKey.id, path: ctx.req.path });
// You can attach data to the session for downstream handlers
// (session changes aren't persisted unless returned in response)
},
};Role-Based Access Control
Enforce roles at the middleware level:
// api/admin/_middleware.js
export default {
handler: async (ctx) => {
if (!ctx.req.session?.userId) {
return { status: 401, error: { code: 'UNAUTHORIZED', message: 'Login required' } };
}
if (ctx.req.session.role !== 'admin') {
return { status: 403, error: { code: 'FORBIDDEN', message: 'Admin access required' } };
}
},
};