Permission Guards
Permissions in Pikku run before your function executes. They're boolean checks that determine whether a request should proceed - if any permission returns false, the request is rejected with a 403 Forbidden.
Permissions run in parallel, so they should be independent checks that don't depend on execution order.
Your First Permission​
A permission is a function that returns a boolean:
import { pikkuPermission } from '#pikku/pikku-types.gen.js'
export const requireAuth = pikkuPermission(async (_services, _data, session) => {
return session?.userId != null
})
export const requireAdmin = pikkuPermission(async (_services, _data, session) => {
return session?.role === 'admin'
})
Use them in your function:
import { pikkuFunc } from '#pikku/pikku-types.gen.js'
export const deleteUser = pikkuFunc<{ userId: string }, void>({
func: async ({ database }, data) => {
await database.delete('users', { where: { id: data.userId } })
},
permissions: {
auth: requireAuth,
admin: requireAdmin
},
docs: {
summary: 'Delete a user',
tags: ['users']
}
})
Both permissions must pass for the function to execute. If either returns false, the request is rejected.
Permission Signature​
pikkuPermission<DataType>(
async (services, data, session) => boolean
)
Parameters:
- services - Your application services (destructure what you need)
- data - The input data (typed with
DataType) - session - The current user session (from
userSession.get())
Return true to allow access, false to deny with 403.
Data-Based Permissions​
Permissions can inspect the request data:
export const requireOwnership = pikkuPermission<{ resourceId: string }>(
async ({ database }, data, session) => {
if (!session?.userId) return false
const resource = await database.query('resources', {
where: { id: data.resourceId }
})
return resource?.ownerId === session.userId
}
)
Use it in a function with matching data type:
export const updateResource = pikkuFunc<
{ resourceId: string; content: string },
Resource
>({
func: async ({ database }, data) => {
return await database.update('resources', {
where: { id: data.resourceId },
data: { content: data.content }
})
},
permissions: {
auth: requireAuth,
owner: requireOwnership // Uses resourceId from data
},
docs: {
summary: 'Update a resource',
tags: ['resources']
}
})
Permission Groups​
You can compose multiple permissions:
export const requirePremium = pikkuPermission(async ({ database }, _data, session) => {
if (!session?.userId) return false
const user = await database.query('users', {
where: { id: session.userId }
})
return user?.isPremium === true
})
// Use multiple permissions together
export const getPremiumContent = pikkuFunc<{ contentId: string }, Content>({
func: async ({ database }, data) => {
return await database.query('premium_content', {
where: { id: data.contentId }
})
},
permissions: {
auth: requireAuth,
premium: requirePremium
},
docs: {
summary: 'Get premium content',
tags: ['content']
}
})
Complex Permissions​
Permissions can perform complex queries:
export const withinQuota = pikkuPermission(async ({ database }, _data, session) => {
if (!session?.userId) return false
const usage = await database.query('api_usage', {
where: {
userId: session.userId,
date: new Date().toISOString().split('T')[0]
}
})
return (usage?.requestCount || 0) < 1000 // Daily limit
})
export const activeSubscription = pikkuPermission(
async ({ database }, _data, session) => {
if (!session?.userId) return false
const sub = await database.query('subscriptions', {
where: { userId: session.userId }
})
if (!sub) return false
return new Date(sub.expiresAt) > new Date()
}
)
HTTP-Specific Permissions​
For HTTP routes, you can apply permissions at the route level or globally:
import { wireHTTP } from '#pikku/pikku-types.gen.js'
// Route-level permissions
wireHTTP({
method: 'delete',
route: '/users/:userId',
func: deleteUser,
permissions: {
auth: requireAuth,
admin: requireAdmin
}
})
Or apply to all routes with a prefix:
import { addHTTPPermission } from '#pikku/pikku-types.gen.js'
// All /admin routes require authentication and admin role
addHTTPPermission('/admin', {
auth: requireAuth,
admin: requireAdmin
})
// All routes require authentication
addHTTPPermission('*', {
auth: requireAuth
})
See HTTP Router for more on addHTTPPermission.
Error Handling​
If a permission throws an error, it's treated as a server error (500), not unauthorized (403):
// âś… Good - returns false for unauthorized
export const requireOwnership = pikkuPermission(async ({ database }, data, session) => {
if (!session?.userId) return false
try {
const resource = await database.query('resources', {
where: { id: data.resourceId }
})
return resource?.ownerId === session.userId
} catch (error) {
// Database error - let it throw (500)
throw error
}
})
// ❌ Bad - throws for unauthorized
export const requireOwnership = pikkuPermission(async ({ database }, data, session) => {
if (!session?.userId) {
throw new Error('Not authenticated') // This returns 500, not 403!
}
// ...
})
Use return false for authorization failures. Only throw for actual errors.
Permission Logic and Execution​
All permissions execute concurrently, not sequentially. This means:
- Don't rely on execution order - Permissions may run in any order
- Avoid side effects - Don't modify shared state or depend on other permissions running first
- Keep them independent - Each permission should be a self-contained check
OR Logic (Default)​
When you list multiple permissions as object keys, any one can pass (OR logic):
permissions: {
admin: requireAdmin, // Can pass if admin
owner: requireOwner, // OR can pass if owner
moderator: requireModerator // OR can pass if moderator
}
// Request proceeds if ANY permission returns true
This is useful when multiple roles or conditions should grant access. For example, both admins and resource owners should be able to edit a resource.
AND Logic (Arrays)​
To require multiple permissions to pass simultaneously, use arrays:
permissions: {
authAndVerified: [requireAuth, requireEmailVerified] // Both must pass
}
// Request proceeds only if ALL permissions in the array return true
Don't depend on execution order - each permission should be an independent check.
Auth vs Permissions​
auth flag controls whether a session is required:
// Requires session to exist (default)
export const getProfile = pikkuFunc({
func: async ({ database }, _data, session) => {
// session is guaranteed to exist
return await database.query('users', { where: { id: session.userId } })
},
auth: true, // Default
docs: {
summary: 'Get user profile',
tags: ['users']
}
})
// No session required
export const getPublicContent = pikkuFunc({
func: async ({ database }, data) => {
return await database.query('content', { where: { id: data.id } })
},
auth: false,
docs: {
summary: 'Get public content',
tags: ['content']
}
})
permissions run additional checks after auth:
export const deleteAccount = pikkuFunc({
func: async ({ database }, _data, session) => {
await database.delete('users', { where: { id: session.userId } })
},
auth: true, // Session required
permissions: {
verified: requireEmailVerified, // Additional check
notBanned: requireNotBanned // Additional check
},
docs: {
summary: 'Delete account',
tags: ['users']
}
})
Reusable Permissions​
Define permissions once, reuse everywhere:
// permissions.ts
export const requireAuth = pikkuPermission(async (_services, _data, session) => {
return session?.userId != null
})
export const requireAdmin = pikkuPermission(async (_services, _data, session) => {
return session?.role === 'admin'
})
export const requireOwnership = pikkuPermission<{ resourceId: string }>(
async ({ database }, data, session) => {
if (!session?.userId) return false
const resource = await database.query('resources', {
where: { id: data.resourceId }
})
return resource?.ownerId === session.userId
}
)
Then import and use:
import { requireAuth, requireAdmin, requireOwnership } from './permissions.js'
export const updateResource = pikkuFunc({
func: async ({ database }, data) => { /* ... */ },
permissions: {
auth: requireAuth,
owner: requireOwnership
},
docs: {
summary: 'Update resource',
tags: ['resources']
}
})
export const deleteUser = pikkuFunc({
func: async ({ database }, data) => { /* ... */ },
permissions: {
auth: requireAuth,
admin: requireAdmin
},
docs: {
summary: 'Delete user',
tags: ['users']
}
})
Best Practices​
Keep permissions focused - One check per permission:
// âś… Good - single responsibility
export const requireAuth = pikkuPermission(...)
export const requireAdmin = pikkuPermission(...)
export const requireVerified = pikkuPermission(...)
permissions: {
auth: requireAuth,
admin: requireAdmin,
verified: requireVerified
}
// ❌ Bad - doing too much
export const requireEverything = pikkuPermission(async (services, data, session) => {
if (!session?.userId) return false
if (session.role !== 'admin') return false
if (!session.emailVerified) return false
// Too many concerns
})
Optimize expensive checks - Cache when possible:
// âś… Good - caches subscription check
export const requireSubscription = pikkuPermission(
async ({ cache, database }, _data, session) => {
if (!session?.userId) return false
const cacheKey = `sub:${session.userId}`
const cached = await cache.get(cacheKey)
if (cached !== null) return cached === 'true'
const sub = await database.query('subscriptions', {
where: { userId: session.userId }
})
const isActive = sub && new Date(sub.expiresAt) > new Date()
await cache.set(cacheKey, isActive ? 'true' : 'false', { ttl: 300 })
return isActive
}
)
Return false, don't throw - For authorization failures:
// âś… Good
if (!session?.userId) return false
// ❌ Bad
if (!session?.userId) throw new Error('Unauthorized') // Returns 500, not 403
Next Steps​
- Functions - Understanding Pikku functions
- HTTP Router - HTTP-specific permissions with
addHTTPPermission - Middleware - Request/response transformation